Skip to content

Commit

Permalink
Merge pull request #10614 from torchiaf/10517-health-agents
Browse files Browse the repository at this point in the history
Add Fleet and Cattle health boxes in Dashboard Page
  • Loading branch information
torchiaf authored Mar 21, 2024
2 parents cc4dbdf + 7c19019 commit 479074f
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 24 deletions.
2 changes: 2 additions & 0 deletions shell/assets/translations/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2065,6 +2065,8 @@ clusterIndexPage:
etcd: Etcd
scheduler: Scheduler
controller-manager: Controller Manager
cattle: Cattle
fleet: Fleet
certs:
label: Certificates

Expand Down
113 changes: 113 additions & 0 deletions shell/pages/c/_cluster/explorer/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { clone } from '@shell/utils/object';
import Dashboard from '@shell/pages/c/_cluster/explorer/index.vue';
import { shallowMount } from '@vue/test-utils';
import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';

describe('page: cluster dashboard', () => {
const mountOptions = {
computed: { monitoringStatus: () => ({ v1: true, v2: true }) },
stubs: {
'n-link': true,
LiveDate: true
},
mocks: {
$store: {
dispatch: jest.fn(),
getters: {
currentCluster: {
id: 'cluster',
metadata: { creationTimestamp: '' },
status: { provider: 'provider' },
},
'cluster/inError': () => false,
'cluster/schemaFor': jest.fn(),
'cluster/all': jest.fn(),
'i18n/exists': jest.fn(),
'i18n/t': jest.fn(),
}
}
},
};

describe.each([
'etcd',
'scheduler',
'controller-manager',
])('%p component health box', (componentId) => {
it.each([
[STATES_ENUM.HEALTHY, 'icon-checkmark', '', []],
[STATES_ENUM.HEALTHY, 'icon-checkmark', `foo`, []],
[STATES_ENUM.HEALTHY, 'icon-checkmark', `${ componentId }foo`, [{ status: 'True' }]],
[STATES_ENUM.UNHEALTHY, 'icon-warning', `${ componentId }foo`, [{ status: 'False' }]],
])('should show %p status', (status, iconClass, name, conditions) => {
const options = clone(mountOptions);

options.mocks.$store.getters.currentCluster.status = {
provider: 'provider',
componentStatuses: [{
name,
conditions
}],
};

const wrapper = shallowMount(Dashboard, options);

const box = wrapper.find(`[data-testid="k8s-service-${ componentId }"]`);
const icon = box.find('i');

expect(box.element).toBeDefined();
expect(box.element.classList).toContain(status);
expect(icon.element.classList).toContain(iconClass);
});
});

describe.each([
['fleet', true, [
[STATES_ENUM.IN_PROGRESS, 'icon-spinner', false, false, '', 0, 0],
[STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, [{ status: 'False' }], 0, 0],
[STATES_ENUM.WARNING, 'icon-warning', true, true, [{ status: 'True' }], 0, 0],
[STATES_ENUM.WARNING, 'icon-warning', true, false, [{ status: 'True' }], 0, 0],
[STATES_ENUM.WARNING, 'icon-warning', true, false, [{ status: 'True' }], 0, 1],
[STATES_ENUM.HEALTHY, 'icon-checkmark', true, false, [{ status: 'True' }], 1, 0],
]],
['cattle', false, [
[STATES_ENUM.IN_PROGRESS, 'icon-spinner', false, false, '', 0, 0],
[STATES_ENUM.UNHEALTHY, 'icon-warning', true, false, [{ status: 'False' }], 0, 0],
[STATES_ENUM.UNHEALTHY, 'icon-warning', true, true, [{ status: 'True' }], 0, 0],
[STATES_ENUM.WARNING, 'icon-warning', true, false, [{ status: 'True' }], 0, 0],
[STATES_ENUM.WARNING, 'icon-warning', true, false, [{ status: 'True' }], 0, 1],
[STATES_ENUM.HEALTHY, 'icon-checkmark', true, false, [{ status: 'True' }], 1, 0],
]]
])('%p agent health box', (agentId, isLocal, statuses) => {
it.each(statuses)('should show %p status', (status, iconClass, isLoaded, disconnected, conditions, readyReplicas, unavailableReplicas) => {
const options = clone(mountOptions);

options.mocks.$store.getters.currentCluster.isLocal = isLocal;

const agent = {
spec: { replicas: 1 },
status: {
readyReplicas,
unavailableReplicas,
conditions
}
};

const wrapper = shallowMount(Dashboard, {
...options,
data: () => ({
[agentId]: isLoaded ? agent : null,
disconnected,
canViewAgents: true
})
});

const box = wrapper.find(`[data-testid="k8s-service-${ agentId }"]`);
const icon = box.find('i');

expect(box.element).toBeDefined();
expect(box.element.classList).toContain(status);
expect(icon.element.classList).toContain(iconClass);
});
});
});
132 changes: 108 additions & 24 deletions shell/pages/c/_cluster/explorer/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
import Certificates from '@shell/components/Certificates';
import { NAME as EXPLORER } from '@shell/config/product/explorer';
import TabTitle from '@shell/components/TabTitle';
import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
export const RESOURCES = [NAMESPACE, INGRESS, PV, WORKLOAD_TYPES.DEPLOYMENT, WORKLOAD_TYPES.STATEFUL_SET, WORKLOAD_TYPES.JOB, WORKLOAD_TYPES.DAEMON_SET, SERVICE];
Expand All @@ -56,7 +57,7 @@ const K8S_METRICS_SUMMARY_URL = '/api/v1/namespaces/cattle-monitoring-system/ser
const ETCD_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-etcd-nodes-1/rancher-etcd-nodes?orgId=1';
const ETCD_METRICS_SUMMARY_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-etcd-1/rancher-etcd?orgId=1';
const COMPONENT_STATUS = [
const CLUSTER_COMPONENTS = [
'etcd',
'scheduler',
'controller-manager',
Expand Down Expand Up @@ -84,7 +85,7 @@ export default {
mixins: [metricPoller],
fetch() {
async fetch() {
fetchClusterResources(this.$store, NODE);
if (this.currentCluster) {
Expand Down Expand Up @@ -114,6 +115,8 @@ export default {
if (this.currentCluster.isLocal && this.$store.getters['management/schemaFor'](MANAGEMENT.NODE)) {
this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE });
}
await this.loadAgents();
}
},
Expand All @@ -128,6 +131,10 @@ export default {
return {
nodeHeaders,
constraints: [],
cattle: null,
fleet: null,
canViewAgents: false,
disconnected: false,
events: [],
nodeMetrics: [],
showClusterMetrics: false,
Expand All @@ -140,6 +147,7 @@ export default {
K8S_METRICS_SUMMARY_URL,
ETCD_METRICS_DETAIL_URL,
ETCD_METRICS_SUMMARY_URL,
STATES_ENUM,
clusterCounts,
selectedTab: 'cluster-events',
extensionCards: getApplicableExtensionEnhancements(this, ExtensionPoint.CARD, CardLocation.CLUSTER_DASHBOARD_CARD, this.$route),
Expand All @@ -154,6 +162,9 @@ export default {
this.$store.dispatch('cluster/forgetType', ENDPOINTS); // Used by AlertTable to get alerts when v2 monitoring is installed
this.$store.dispatch('cluster/forgetType', METRIC.NODE);
this.$store.dispatch('cluster/forgetType', MANAGEMENT.NODE);
this.$store.dispatch('cluster/forgetType', WORKLOAD_TYPES.DEPLOYMENT);
clearInterval(this.interval);
},
computed: {
Expand Down Expand Up @@ -212,18 +223,34 @@ export default {
return allowedResources.filter((resource) => this.$store.getters['cluster/schemaFor'](resource));
},
componentServices() {
const status = [];
clusterServices() {
const services = [];
COMPONENT_STATUS.forEach((cs) => {
status.push({
CLUSTER_COMPONENTS.forEach((cs) => {
services.push({
name: cs,
healthy: this.isComponentStatusHealthy(cs),
status: this.getComponentStatus(cs),
labelKey: `clusterIndexPage.sections.componentStatus.${ cs }`,
});
});
return status;
if (this.canViewAgents) {
if (!this.currentCluster.isLocal) {
services.push({
name: 'cattle',
status: this.getAgentStatus(this.cattle, this.disconnected),
labelKey: 'clusterIndexPage.sections.componentStatus.cattle',
});
}
services.push({
name: 'fleet',
status: this.getAgentStatus(this.fleet),
labelKey: 'clusterIndexPage.sections.componentStatus.fleet',
});
}
return services;
},
totalCountGaugeInput() {
Expand Down Expand Up @@ -372,13 +399,35 @@ export default {
},
methods: {
// Ported from Ember
isComponentStatusHealthy(field) {
async loadAgents() {
this.canViewAgents = !!this.$store.getters['cluster/schemaFor'](WORKLOAD_TYPES.DEPLOYMENT);
if (this.canViewAgents) {
if (!this.currentCluster.isLocal) {
this.cattle = await this.$store.dispatch('cluster/find', {
type: WORKLOAD_TYPES.DEPLOYMENT,
id: 'cattle-system/cattle-cluster-agent'
});
// Scaling Up/Down cattle deployment causes web sockets disconnection;
this.interval = setInterval(() => {
this.disconnected = !!this.$store.getters['cluster/inError']({ type: NODE });
}, 1000);
}
this.fleet = await this.$store.dispatch('cluster/find', {
type: WORKLOAD_TYPES.DEPLOYMENT,
id: `${ this.currentCluster.isLocal ? 'cattle-fleet-local-system' : 'cattle-fleet-system' }/fleet-agent`
});
}
},
getComponentStatus(field) {
const matching = (this.currentCluster?.status?.componentStatuses || []).filter((s) => s.name.startsWith(field));
// If there's no matching component status, it's "healthy"
if ( !matching.length ) {
return true;
return STATES_ENUM.HEALTHY;
}
const count = matching.reduce((acc, status) => {
Expand All @@ -387,7 +436,27 @@ export default {
return !conditions ? acc : acc + 1;
}, 0);
return count === 0;
if (count > 0) {
return STATES_ENUM.UNHEALTHY;
}
return STATES_ENUM.HEALTHY;
},
getAgentStatus(agent, disconnected = false) {
if (!agent) {
return STATES_ENUM.IN_PROGRESS;
}
if (disconnected || agent.status.conditions.find((c) => c.status !== 'True')) {
return STATES_ENUM.UNHEALTHY;
}
if (agent.spec.replicas !== agent.status.readyReplicas || agent.status.unavailableReplicas > 0) {
return STATES_ENUM.WARNING;
}
return STATES_ENUM.HEALTHY;
},
showActions() {
Expand Down Expand Up @@ -547,22 +616,29 @@ export default {
/>
</div>
<div v-if="!hasV1Monitoring && componentServices">
<div v-if="!hasV1Monitoring && clusterServices">
<div
v-for="status in componentServices"
:key="status.name"
class="k8s-component-status"
:class="{'k8s-component-status-healthy': status.healthy, 'k8s-component-status-unhealthy': !status.healthy}"
v-for="service in clusterServices"
:key="service.name"
class="k8s-service-status"
:class="{[service.status]: true }"
:data-testid="`k8s-service-${ service.name }`"
>
<i
v-if="status.healthy"
v-if="service.status === STATES_ENUM.IN_PROGRESS"
class="icon icon-spinner icon-spin"
/>
<i
v-else-if="service.status === STATES_ENUM.HEALTHY"
class="icon icon-checkmark"
/>
<i
v-else
class="icon icon-warning"
/>
<div>{{ t(status.labelKey) }}</div>
<div class="label">
{{ t(service.labelKey) }}
</div>
</div>
</div>
Expand Down Expand Up @@ -747,12 +823,17 @@ export default {
margin-bottom: 20px;
}
.k8s-component-status {
.k8s-service-status {
align-items: center;
display: inline-flex;
border: 1px solid;
border-color: var(--border);
margin-top: 20px;
.label {
border-left: 1px solid var(--border);
}
&:not(:last-child) {
margin-right: 20px;
}
Expand All @@ -764,20 +845,23 @@ export default {
> I {
text-align: center;
padding: 5px 10px;
border-right: 1px solid var(--border);
}
&.k8s-component-status-unhealthy {
&.unhealthy {
border-color: var(--error-border);
> I {
color: var(--error)
}
}
&.k8s-component-status-healthy {
border-color: var(--border);
&.warning {
> I {
color: var(--warning)
}
}
&.healthy {
> I {
color: var(--success)
}
Expand Down

0 comments on commit 479074f

Please sign in to comment.