feat(dashboard): show Longhorn and CloudNativePG operators with replica counts
This commit is contained in:
@@ -102,8 +102,10 @@ func (c *Collector) Collect() (*ClusterStatus, error) {
|
||||
var wg sync.WaitGroup
|
||||
var nodesErr, metricsErr, ingressErr error
|
||||
var rawNodes, rawMetrics, rawIngresses []byte
|
||||
var rawCnpgDeploy, rawCnpgClusters, rawLonghornDs, rawLonghornDeploy, rawLonghornVols []byte
|
||||
var cnpgDeployErr, cnpgClustersErr, longhornDsErr, longhornDeployErr, longhornVolsErr error
|
||||
|
||||
wg.Add(3)
|
||||
wg.Add(8)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
rawNodes, nodesErr = c.k8sGet("/api/v1/nodes")
|
||||
@@ -116,6 +118,26 @@ func (c *Collector) Collect() (*ClusterStatus, error) {
|
||||
defer wg.Done()
|
||||
rawIngresses, ingressErr = c.k8sGet("/apis/networking.k8s.io/v1/ingresses")
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
rawCnpgDeploy, cnpgDeployErr = c.k8sGet("/apis/apps/v1/namespaces/cnpg-system/deployments")
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
rawCnpgClusters, cnpgClustersErr = c.k8sGet("/apis/postgresql.cnpg.io/v1/namespaces/default/clusters")
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
rawLonghornDs, longhornDsErr = c.k8sGet("/apis/apps/v1/namespaces/longhorn-system/daemonsets")
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
rawLonghornDeploy, longhornDeployErr = c.k8sGet("/apis/apps/v1/namespaces/longhorn-system/deployments")
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
rawLonghornVols, longhornVolsErr = c.k8sGet("/apis/longhorn.io/v1beta2/namespaces/longhorn-system/volumes")
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
if nodesErr != nil {
|
||||
@@ -301,6 +323,131 @@ func (c *Collector) Collect() (*ClusterStatus, error) {
|
||||
status.Summary.TotalIngresses = totalIng
|
||||
}
|
||||
|
||||
// Populate CloudNativePG Operator & Cluster info
|
||||
status.Operators.Postgres = PostgresOperatorInfo{
|
||||
Name: "CloudNativePG",
|
||||
Namespace: "cnpg-system",
|
||||
Status: "Healthy",
|
||||
}
|
||||
if cnpgDeployErr == nil && len(rawCnpgDeploy) > 0 {
|
||||
var deployList struct {
|
||||
Items []struct {
|
||||
Metadata struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"metadata"`
|
||||
Spec struct {
|
||||
Replicas int `json:"replicas"`
|
||||
} `json:"spec"`
|
||||
Status struct {
|
||||
ReadyReplicas int `json:"readyReplicas"`
|
||||
Replicas int `json:"replicas"`
|
||||
} `json:"status"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(rawCnpgDeploy, &deployList); err == nil {
|
||||
for _, item := range deployList.Items {
|
||||
if strings.Contains(item.Metadata.Name, "cnpg") || strings.Contains(item.Metadata.Name, "cloudnative-pg") {
|
||||
status.Operators.Postgres.OperatorReplicas = item.Spec.Replicas
|
||||
status.Operators.Postgres.OperatorReady = item.Status.ReadyReplicas
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cnpgClustersErr == nil && len(rawCnpgClusters) > 0 {
|
||||
var clusterList struct {
|
||||
Items []struct {
|
||||
Metadata struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"metadata"`
|
||||
Spec struct {
|
||||
Instances int `json:"instances"`
|
||||
} `json:"spec"`
|
||||
Status struct {
|
||||
Instances int `json:"instances"`
|
||||
ReadyInstances int `json:"readyInstances"`
|
||||
Phase string `json:"phase"`
|
||||
TargetPrimary string `json:"targetPrimary"`
|
||||
} `json:"status"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(rawCnpgClusters, &clusterList); err == nil && len(clusterList.Items) > 0 {
|
||||
cItem := clusterList.Items[0]
|
||||
status.Operators.Postgres.ClusterName = cItem.Metadata.Name
|
||||
status.Operators.Postgres.Instances = cItem.Spec.Instances
|
||||
status.Operators.Postgres.ReadyInstances = cItem.Status.ReadyInstances
|
||||
status.Operators.Postgres.Primary = cItem.Status.TargetPrimary
|
||||
if cItem.Status.Phase != "" {
|
||||
status.Operators.Postgres.Status = cItem.Status.Phase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate Longhorn Storage Operator info
|
||||
status.Operators.Longhorn = LonghornOperatorInfo{
|
||||
Name: "Longhorn",
|
||||
Namespace: "longhorn-system",
|
||||
StorageClass: "longhorn-2-replicas",
|
||||
Status: "Healthy",
|
||||
}
|
||||
if longhornDsErr == nil && len(rawLonghornDs) > 0 {
|
||||
var dsList struct {
|
||||
Items []struct {
|
||||
Metadata struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"metadata"`
|
||||
Status struct {
|
||||
DesiredNumberScheduled int `json:"desiredNumberScheduled"`
|
||||
NumberReady int `json:"numberReady"`
|
||||
} `json:"status"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(rawLonghornDs, &dsList); err == nil {
|
||||
for _, item := range dsList.Items {
|
||||
if item.Metadata.Name == "longhorn-manager" {
|
||||
status.Operators.Longhorn.ManagerDesired = item.Status.DesiredNumberScheduled
|
||||
status.Operators.Longhorn.ManagerReady = item.Status.NumberReady
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if longhornDeployErr == nil && len(rawLonghornDeploy) > 0 {
|
||||
var deployList struct {
|
||||
Items []struct {
|
||||
Metadata struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"metadata"`
|
||||
Spec struct {
|
||||
Replicas int `json:"replicas"`
|
||||
} `json:"spec"`
|
||||
Status struct {
|
||||
ReadyReplicas int `json:"readyReplicas"`
|
||||
} `json:"status"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(rawLonghornDeploy, &deployList); err == nil {
|
||||
for _, item := range deployList.Items {
|
||||
if item.Metadata.Name == "longhorn-ui" {
|
||||
status.Operators.Longhorn.UIDesired = item.Spec.Replicas
|
||||
status.Operators.Longhorn.UIReady = item.Status.ReadyReplicas
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if longhornVolsErr == nil && len(rawLonghornVols) > 0 {
|
||||
var volList struct {
|
||||
Items []json.RawMessage `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(rawLonghornVols, &volList); err == nil {
|
||||
status.Operators.Longhorn.TotalVolumes = len(volList.Items)
|
||||
}
|
||||
}
|
||||
if status.Operators.Longhorn.ManagerReady < status.Operators.Longhorn.ManagerDesired && status.Operators.Longhorn.ManagerDesired > 0 {
|
||||
status.Operators.Longhorn.Status = "Degraded"
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +198,38 @@
|
||||
.node-meta { font-size: 12px; color: var(--text-muted); display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 12px 0 16px 0; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px; }
|
||||
.node-metric { margin-top: 10px; }
|
||||
.metric-label-row { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 4px; }
|
||||
/* Operators Grid */
|
||||
.operators-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.operator-card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
.operator-card:hover { transform: translateY(-2px); border-color: rgba(56, 189, 248, 0.4); }
|
||||
.operator-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
||||
.operator-title { font-size: 16px; font-weight: 600; color: #fff; display: flex; align-items: center; gap: 8px; }
|
||||
.operator-sub { font-size: 11.5px; color: var(--text-muted); margin-top: 3px; }
|
||||
.operator-meta {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin: 12px 0 0 0;
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.operator-stat-box { display: flex; flex-direction: column; gap: 3px; }
|
||||
.operator-stat-label { font-size: 10.5px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
.operator-stat-val { font-size: 13.5px; font-weight: 600; color: #fff; word-break: break-all; }
|
||||
|
||||
/* Zero-Scale Grid */
|
||||
.zero-grid {
|
||||
@@ -414,6 +446,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Platform Operators Section -->
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<span>⚙️ Core Platform Operators</span>
|
||||
<span class="badge-count" id="count-operators">2 Active</span>
|
||||
</div>
|
||||
<div style="font-size:12px; color:var(--text-muted);">High-Availability Data & Distributed Storage Controllers</div>
|
||||
</div>
|
||||
<div class="operators-grid" id="operators-container">
|
||||
<div style="color:var(--text-muted); font-size:13px;">Querying platform operators...</div>
|
||||
</div>
|
||||
|
||||
<!-- Nodes Section -->
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
@@ -614,6 +658,82 @@ function renderDashboard(data) {
|
||||
document.getElementById('count-zero').innerText = sum.total_zero_apps;
|
||||
document.getElementById('sub-zero-active').innerText = `${sum.active_zero_apps} Active Now`;
|
||||
|
||||
// Render Platform Operators
|
||||
const opsCont = document.getElementById('operators-container');
|
||||
if (opsCont && data.operators) {
|
||||
opsCont.innerHTML = '';
|
||||
const pg = data.operators.postgres;
|
||||
const lh = data.operators.longhorn;
|
||||
|
||||
if (pg) {
|
||||
const pgCard = document.createElement('div');
|
||||
pgCard.className = 'operator-card';
|
||||
const pgReadyCls = pg.operator_ready > 0 ? 'ready' : 'notready';
|
||||
pgCard.innerHTML = `
|
||||
<div class="operator-top">
|
||||
<div>
|
||||
<div class="operator-title"><span>🐘</span> CloudNativePG Operator</div>
|
||||
<div class="operator-sub">Enterprise PostgreSQL HA Controller • ns: ${pg.namespace || 'cnpg-system'}</div>
|
||||
</div>
|
||||
<span class="status-badge ${pgReadyCls}">${pg.status || 'Active'}</span>
|
||||
</div>
|
||||
<div class="operator-meta">
|
||||
<div class="operator-stat-box">
|
||||
<span class="operator-stat-label">Operator Controller</span>
|
||||
<span class="operator-stat-val" style="color: var(--green);">${pg.operator_ready} / ${pg.operator_replicas} Replicas Ready</span>
|
||||
</div>
|
||||
<div class="operator-stat-box">
|
||||
<span class="operator-stat-label">HA Database Cluster</span>
|
||||
<span class="operator-stat-val">${pg.ready_instances} / ${pg.instances} Instances Ready</span>
|
||||
</div>
|
||||
<div class="operator-stat-box">
|
||||
<span class="operator-stat-label">Primary Instance</span>
|
||||
<span class="operator-stat-val" style="color: var(--blue);">🟢 ${pg.primary || 'ha-postgres-1'}</span>
|
||||
</div>
|
||||
<div class="operator-stat-box">
|
||||
<span class="operator-stat-label">Cluster Target</span>
|
||||
<span class="operator-stat-val">${pg.cluster_name || 'ha-postgres'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
opsCont.appendChild(pgCard);
|
||||
}
|
||||
|
||||
if (lh) {
|
||||
const lhCard = document.createElement('div');
|
||||
lhCard.className = 'operator-card';
|
||||
const lhReadyCls = lh.manager_ready > 0 && lh.manager_ready === lh.manager_desired ? 'ready' : 'notready';
|
||||
lhCard.innerHTML = `
|
||||
<div class="operator-top">
|
||||
<div>
|
||||
<div class="operator-title"><span>🐮</span> Longhorn Storage Operator</div>
|
||||
<div class="operator-sub">Cloud-Native Distributed Block Storage • ns: ${lh.namespace || 'longhorn-system'}</div>
|
||||
</div>
|
||||
<span class="status-badge ${lhReadyCls}">${lh.status || 'Healthy'}</span>
|
||||
</div>
|
||||
<div class="operator-meta">
|
||||
<div class="operator-stat-box">
|
||||
<span class="operator-stat-label">Storage Manager (DS)</span>
|
||||
<span class="operator-stat-val" style="color: var(--green);">${lh.manager_ready} / ${lh.manager_desired} Nodes Ready</span>
|
||||
</div>
|
||||
<div class="operator-stat-box">
|
||||
<span class="operator-stat-label">Management UI</span>
|
||||
<span class="operator-stat-val" style="color: var(--green);">${lh.ui_ready} / ${lh.ui_desired} Replicas Ready</span>
|
||||
</div>
|
||||
<div class="operator-stat-box">
|
||||
<span class="operator-stat-label">Distributed Volumes</span>
|
||||
<span class="operator-stat-val" style="color: var(--blue);">${lh.total_volumes} Active PVs</span>
|
||||
</div>
|
||||
<div class="operator-stat-box">
|
||||
<span class="operator-stat-label">Storage Class</span>
|
||||
<span class="operator-stat-val">${lh.storage_class || 'longhorn-2-replicas'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
opsCont.appendChild(lhCard);
|
||||
}
|
||||
}
|
||||
|
||||
// Render Nodes
|
||||
const nodesCont = document.getElementById('nodes-container');
|
||||
nodesCont.innerHTML = '';
|
||||
|
||||
@@ -3,14 +3,44 @@ package main
|
||||
import "time"
|
||||
|
||||
type ClusterStatus struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
ClusterVersion string `json:"cluster_version"`
|
||||
Authenticated bool `json:"authenticated"`
|
||||
Summary ClusterSummary `json:"summary"`
|
||||
Nodes []NodeStatus `json:"nodes"`
|
||||
ZeroScaleApps []ZeroScaleApp `json:"zero_scale_apps"`
|
||||
OtherSites []SiteGroup `json:"other_sites"`
|
||||
GitURL string `json:"git_url"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
ClusterVersion string `json:"cluster_version"`
|
||||
Authenticated bool `json:"authenticated"`
|
||||
Summary ClusterSummary `json:"summary"`
|
||||
Operators PlatformOperators `json:"operators"`
|
||||
Nodes []NodeStatus `json:"nodes"`
|
||||
ZeroScaleApps []ZeroScaleApp `json:"zero_scale_apps"`
|
||||
OtherSites []SiteGroup `json:"other_sites"`
|
||||
GitURL string `json:"git_url"`
|
||||
}
|
||||
|
||||
type PlatformOperators struct {
|
||||
Postgres PostgresOperatorInfo `json:"postgres"`
|
||||
Longhorn LonghornOperatorInfo `json:"longhorn"`
|
||||
}
|
||||
|
||||
type PostgresOperatorInfo struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Status string `json:"status"`
|
||||
OperatorReplicas int `json:"operator_replicas"`
|
||||
OperatorReady int `json:"operator_ready"`
|
||||
ClusterName string `json:"cluster_name"`
|
||||
Instances int `json:"instances"`
|
||||
ReadyInstances int `json:"ready_instances"`
|
||||
Primary string `json:"primary"`
|
||||
}
|
||||
|
||||
type LonghornOperatorInfo struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Status string `json:"status"`
|
||||
ManagerDesired int `json:"manager_desired"`
|
||||
ManagerReady int `json:"manager_ready"`
|
||||
UIDesired int `json:"ui_desired"`
|
||||
UIReady int `json:"ui_ready"`
|
||||
StorageClass string `json:"storage_class"`
|
||||
TotalVolumes int `json:"total_volumes"`
|
||||
}
|
||||
|
||||
type ClusterSummary struct {
|
||||
|
||||
Reference in New Issue
Block a user