Skip to content

Commit d068dd4

Browse files
authored
Merge pull request #67 from kaskol10/fix/topology-view
Fix topology view behavior when backend endpoint is missing
2 parents fe1352d + 4fa6715 commit d068dd4

2 files changed

Lines changed: 126 additions & 12 deletions

File tree

frontend/src/components/TopologyView.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,20 +192,61 @@ export default function TopologyView() {
192192
const [nodes, setNodes] = useState([]);
193193
const [loading, setLoading] = useState(false);
194194
const [error, setError] = useState(null);
195+
const [fallbackMode, setFallbackMode] = useState(false);
195196
const [metric, setMetric] = useState('cpu');
196197
const [nodePoolFilter, setNodePoolFilter] = useState('');
197198
const [zoneFilter, setZoneFilter] = useState('');
198199
const [nodeNameFilter, setNodeNameFilter] = useState('');
199200
const [podNsFilter, setPodNsFilter] = useState('');
200201
const [podNameFilter, setPodNameFilter] = useState('');
201202

203+
const mapNodesFallback = (nodeItems) =>
204+
nodeItems.map((node) => ({
205+
...node,
206+
// Older backends don't provide /topology; synthesize minimal pod objects from podNames.
207+
pods: (node.podNames || []).map((qualifiedName) => {
208+
const slashIdx = qualifiedName.indexOf('/');
209+
const namespace = slashIdx > 0 ? qualifiedName.slice(0, slashIdx) : 'default';
210+
const name = slashIdx > 0 ? qualifiedName.slice(slashIdx + 1) : qualifiedName;
211+
return {
212+
name,
213+
namespace,
214+
nodeName: node.name,
215+
workloadName: '',
216+
workloadType: '',
217+
qosClass: '',
218+
// Equal weights in fallback mode; accurate requests require /topology support.
219+
requests: {
220+
cpuCores: 1,
221+
memoryGiB: 1,
222+
},
223+
};
224+
}),
225+
}));
226+
202227
const fetchTopology = useCallback(async () => {
203228
setLoading(true);
204229
setError(null);
230+
setFallbackMode(false);
205231
try {
206232
const response = await axios.get(`${API_URL}/api/v1/topology`);
207233
setNodes(response.data.nodes || []);
208234
} catch (err) {
235+
if (err.response?.status === 404) {
236+
try {
237+
const nodesResponse = await axios.get(`${API_URL}/api/v1/nodes`);
238+
setNodes(mapNodesFallback(nodesResponse.data.nodes || []));
239+
setFallbackMode(true);
240+
return;
241+
} catch (fallbackErr) {
242+
setError(
243+
fallbackErr.response?.data?.error ||
244+
fallbackErr.message ||
245+
'Failed to load topology',
246+
);
247+
return;
248+
}
249+
}
209250
setError(err.response?.data?.error || err.message || 'Failed to load topology');
210251
} finally {
211252
setLoading(false);
@@ -379,6 +420,16 @@ export default function TopologyView() {
379420
<AlertDescription>{error}</AlertDescription>
380421
</Alert>
381422
)}
423+
{fallbackMode && (
424+
<Alert>
425+
<AlertTitle>Compatibility mode</AlertTitle>
426+
<AlertDescription>
427+
Backend endpoint <code>/api/v1/topology</code> is not available. Showing pod placement
428+
using <code>/api/v1/nodes</code> data with equal pod sizing. Upgrade backend image to a
429+
version that includes topology support for accurate request-based sizing.
430+
</AlertDescription>
431+
</Alert>
432+
)}
382433

383434
{loading && nodes.length === 0 ? (
384435
<div className="flex justify-center py-16">

internal/api/server.go

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,16 +1062,34 @@ type TopologyNode struct {
10621062
CreationTime string `json:"creationTime,omitempty"`
10631063
}
10641064

1065-
func topologyRequestsFromPod(p kubernetes.PodInfo) TopologyPodResources {
1065+
func (s *Server) topologyRequestsFromPod(p kubernetes.PodInfo) TopologyPodResources {
10661066
var out TopologyPodResources
10671067
if p.Requests.CPU != "" {
10681068
if q, err := resource.ParseQuantity(p.Requests.CPU); err == nil {
10691069
out.CPUCores = float64(q.MilliValue()) / 1000.0
1070+
} else {
1071+
debugLog(
1072+
s.config.Debug,
1073+
"[topology] failed to parse CPU request for pod %s/%s (%q): %v\n",
1074+
p.Namespace,
1075+
p.Name,
1076+
p.Requests.CPU,
1077+
err,
1078+
)
10701079
}
10711080
}
10721081
if p.Requests.Memory != "" {
10731082
if q, err := resource.ParseQuantity(p.Requests.Memory); err == nil {
10741083
out.MemoryGiB = float64(q.Value()) / (1024 * 1024 * 1024)
1084+
} else {
1085+
debugLog(
1086+
s.config.Debug,
1087+
"[topology] failed to parse memory request for pod %s/%s (%q): %v\n",
1088+
p.Namespace,
1089+
p.Name,
1090+
p.Requests.Memory,
1091+
err,
1092+
)
10751093
}
10761094
}
10771095
return out
@@ -1101,17 +1119,32 @@ func (s *Server) getTopology(c *gin.Context) {
11011119
if maxPodsPerNode > 1000 {
11021120
maxPodsPerNode = 1000
11031121
}
1122+
} else {
1123+
debugLog(
1124+
s.config.Debug,
1125+
"[topology] invalid maxPodsPerNode query value %q, using default %d\n",
1126+
v,
1127+
maxPodsPerNode,
1128+
)
11041129
}
11051130
}
1131+
debugLog(
1132+
s.config.Debug,
1133+
"[topology] request received remote=%s maxPodsPerNode=%d\n",
1134+
c.ClientIP(),
1135+
maxPodsPerNode,
1136+
)
11061137

11071138
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
11081139
defer cancel()
11091140

11101141
nodes, err := s.k8sClient.GetAllNodesWithUsage(ctx)
11111142
if err != nil {
1143+
debugLog(s.config.Debug, "[topology] failed to fetch nodes: %v\n", err)
11121144
c.JSON(500, gin.H{"error": err.Error()})
11131145
return
11141146
}
1147+
debugLog(s.config.Debug, "[topology] fetched %d nodes\n", len(nodes))
11151148

11161149
nodeNames := make(map[string]bool, len(nodes))
11171150
for _, n := range nodes {
@@ -1120,39 +1153,68 @@ func (s *Server) getTopology(c *gin.Context) {
11201153

11211154
allPods, err := s.k8sClient.GetPodsOnNodes(ctx, nodeNames)
11221155
if err != nil {
1156+
debugLog(s.config.Debug, "[topology] failed to fetch pods on nodes: %v\n", err)
11231157
c.JSON(500, gin.H{"error": err.Error()})
11241158
return
11251159
}
1160+
debugLog(s.config.Debug, "[topology] fetched %d pods across selected nodes\n", len(allPods))
11261161

11271162
podsByNode := make(map[string][]kubernetes.PodInfo)
1163+
var terminalSkipped int
11281164
for _, p := range allPods {
11291165
if p.Phase == "Succeeded" || p.Phase == "Failed" {
1166+
terminalSkipped++
11301167
continue
11311168
}
11321169
podsByNode[p.NodeName] = append(podsByNode[p.NodeName], p)
11331170
}
1171+
debugLog(
1172+
s.config.Debug,
1173+
"[topology] grouped pods by node: nodesWithPods=%d terminalPodsSkipped=%d\n",
1174+
len(podsByNode),
1175+
terminalSkipped,
1176+
)
11341177

11351178
out := make([]TopologyNode, 0, len(nodes))
11361179
for _, node := range nodes {
1137-
list := podsByNode[node.Name]
1180+
type podWithReq struct {
1181+
pod kubernetes.PodInfo
1182+
req TopologyPodResources
1183+
}
1184+
1185+
list := make([]podWithReq, 0, len(podsByNode[node.Name]))
1186+
for _, p := range podsByNode[node.Name] {
1187+
list = append(list, podWithReq{
1188+
pod: p,
1189+
req: s.topologyRequestsFromPod(p),
1190+
})
1191+
}
1192+
11381193
sort.Slice(list, func(i, j int) bool {
1139-
return topologyRequestsFromPod(list[i]).CPUCores > topologyRequestsFromPod(list[j]).CPUCores
1194+
return list[i].req.CPUCores > list[j].req.CPUCores
11401195
})
1196+
originalLen := len(list)
11411197
if len(list) > maxPodsPerNode {
11421198
list = list[:maxPodsPerNode]
1199+
debugLog(
1200+
s.config.Debug,
1201+
"[topology] truncated pods for node=%s from %d to %d by maxPodsPerNode\n",
1202+
node.Name,
1203+
originalLen,
1204+
len(list),
1205+
)
11431206
}
11441207

11451208
topPods := make([]TopologyPod, 0, len(list))
1146-
for _, p := range list {
1147-
req := topologyRequestsFromPod(p)
1209+
for _, entry := range list {
11481210
topPods = append(topPods, TopologyPod{
1149-
Name: p.Name,
1150-
Namespace: p.Namespace,
1151-
NodeName: p.NodeName,
1152-
WorkloadName: p.WorkloadName,
1153-
WorkloadType: p.WorkloadType,
1154-
QOSClass: p.QOSClass,
1155-
Requests: req,
1211+
Name: entry.pod.Name,
1212+
Namespace: entry.pod.Namespace,
1213+
NodeName: entry.pod.NodeName,
1214+
WorkloadName: entry.pod.WorkloadName,
1215+
WorkloadType: entry.pod.WorkloadType,
1216+
QOSClass: entry.pod.QOSClass,
1217+
Requests: entry.req,
11561218
})
11571219
}
11581220

@@ -1170,6 +1232,7 @@ func (s *Server) getTopology(c *gin.Context) {
11701232
CreationTime: node.CreationTime,
11711233
})
11721234
}
1235+
debugLog(s.config.Debug, "[topology] returning topology response with %d nodes\n", len(out))
11731236

11741237
c.JSON(200, gin.H{
11751238
"nodes": out,

0 commit comments

Comments
 (0)