Skip to content

Commit c258641

Browse files
chifu1234Kevin Klopfenstein
andauthored
feat: #18 implement vm migrating feature (#24)
* feat: #18 implement vm migrating feature * feat: #18 implement vm migrating feature --------- Co-authored-by: Kevin Klopfenstein <[email protected]>
1 parent 8e9301e commit c258641

File tree

6 files changed

+195
-73
lines changed

6 files changed

+195
-73
lines changed

kubevirt/.eslintcache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
[{"/home/klokev20/headlamp-plugins/kubevirt/src/headlamp-plugin.d.ts":"1","/home/klokev20/headlamp-plugins/kubevirt/src/index.tsx":"2","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/Terminal/Terminal.tsx":"3","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx":"4","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/List.tsx":"5","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/VirtualMachineInstance.tsx":"6","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/Details.tsx":"7","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/List.tsx":"8","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx":"9","/home/klokev20/headlamp-plugins/kubevirt/src/storybook.test.tsx":"10"},{"size":680,"mtime":1762330749267,"results":"11","hashOfConfig":"12"},{"size":1575,"mtime":1762333810208,"results":"13","hashOfConfig":"12"},{"size":7407,"mtime":1762330749267,"results":"14","hashOfConfig":"12"},{"size":2486,"mtime":1762330749267,"results":"15","hashOfConfig":"12"},{"size":3772,"mtime":1762334239548,"results":"16","hashOfConfig":"12"},{"size":1180,"mtime":1762330749267,"results":"17","hashOfConfig":"12"},{"size":5301,"mtime":1762330749267,"results":"18","hashOfConfig":"12"},{"size":3379,"mtime":1762334402395,"results":"19","hashOfConfig":"12"},{"size":1203,"mtime":1762330749267,"results":"20","hashOfConfig":"12"},{"size":176,"mtime":1762330749267,"results":"21","hashOfConfig":"12"},{"filePath":"22","messages":"23","suppressedMessages":"24","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1o2l0oj",{"filePath":"25","messages":"26","suppressedMessages":"27","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"28","messages":"29","suppressedMessages":"30","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"31","messages":"32","suppressedMessages":"33","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"34","messages":"35","suppressedMessages":"36","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"37","messages":"38","suppressedMessages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"40","messages":"41","suppressedMessages":"42","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"43","messages":"44","suppressedMessages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"46","messages":"47","suppressedMessages":"48","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"49","messages":"50","suppressedMessages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/klokev20/headlamp-plugins/kubevirt/src/headlamp-plugin.d.ts",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/index.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/Terminal/Terminal.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/List.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/VirtualMachineInstance.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/Details.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/List.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/storybook.test.tsx",[],[]]
1+
[{"/home/klokev20/headlamp-plugins/kubevirt/src/headlamp-plugin.d.ts":"1","/home/klokev20/headlamp-plugins/kubevirt/src/index.tsx":"2","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/Terminal/Terminal.tsx":"3","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx":"4","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/List.tsx":"5","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/VirtualMachineInstance.tsx":"6","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/Details.tsx":"7","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/List.tsx":"8","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx":"9","/home/klokev20/headlamp-plugins/kubevirt/src/storybook.test.tsx":"10"},{"size":680,"mtime":1762503095333,"results":"11","hashOfConfig":"12"},{"size":1575,"mtime":1762503095333,"results":"13","hashOfConfig":"12"},{"size":7407,"mtime":1762503095334,"results":"14","hashOfConfig":"12"},{"size":2486,"mtime":1762503095334,"results":"15","hashOfConfig":"12"},{"size":3772,"mtime":1762503095334,"results":"16","hashOfConfig":"12"},{"size":1636,"mtime":1762503095334,"results":"17","hashOfConfig":"12"},{"size":12085,"mtime":1762508534665,"results":"18","hashOfConfig":"12"},{"size":3379,"mtime":1762503095334,"results":"19","hashOfConfig":"12"},{"size":2211,"mtime":1762507882178,"results":"20","hashOfConfig":"12"},{"size":176,"mtime":1762503095334,"results":"21","hashOfConfig":"12"},{"filePath":"22","messages":"23","suppressedMessages":"24","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1o2l0oj",{"filePath":"25","messages":"26","suppressedMessages":"27","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"28","messages":"29","suppressedMessages":"30","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"31","messages":"32","suppressedMessages":"33","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"34","messages":"35","suppressedMessages":"36","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"37","messages":"38","suppressedMessages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"40","messages":"41","suppressedMessages":"42","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"43","messages":"44","suppressedMessages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"46","messages":"47","suppressedMessages":"48","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"49","messages":"50","suppressedMessages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/klokev20/headlamp-plugins/kubevirt/src/headlamp-plugin.d.ts",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/index.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/Terminal/Terminal.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/List.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/VirtualMachineInstance.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/Details.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/List.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/storybook.test.tsx",[],[]]

kubevirt/artifacthub-pkg.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
version: v0.0.1-beta5
1+
version: v0.0.1-beta6
22
name: buttah_kubevirt
33
displayName: Kubevirt
44
createdAt: "2025-01-01T00:00:00Z"
55
logoURL: "https://github.com/kubevirt/community/blob/main/logo/KubeVirt_icon.png?raw=true"
66
description: "A plugin for managing KubeVirt virtual machines within a Kubernetes cluster."
77
annotations:
8-
headlamp/plugin/archive-url: "https://github.com/buttahtoast/headlamp-plugins/releases/download/kubevirt-v0.0.1-beta5/kubevirt-v0.0.1-beta5.tar.gz"
9-
headlamp/plugin/archive-checksum: "SHA256:77cfd22a90964804b16780f66c627f4e913223f6a6a4b2e0e5efb2b24ba36467"
8+
headlamp/plugin/archive-url: "https://github.com/buttahtoast/headlamp-plugins/releases/download/kubevirt-v0.0.1-beta6/kubevirt-v0.0.1-beta6.tar.gz"
9+
headlamp/plugin/archive-checksum: "SHA256:68a98b4204f182154b55f1ac899461a6112cf1698ba226da4991d44c0453da8a"
1010
headlamp/plugin/version-compat: ">=0.24"
1111
headlamp/plugin/distro-compat: "in-cluster,web,docker-desktop,desktop"
1212
links:

kubevirt/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "kubevirt",
3-
"version": "v0.0.1-beta5",
3+
"version": "v0.0.1-beta6",
44
"description": "A plugin for managing KubeVirt virtual machines within a Kubernetes cluster.",
55
"scripts": {
66
"start": "headlamp-plugin start",

kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default function VirtualMachineInstanceDetails(props: VirtualMachineInsta
2222
name={name}
2323
namespace={namespace}
2424
resourceType={VirtualMachineInstance}
25+
withEvents
2526
extraInfo={item =>
2627
item && [
2728
{

kubevirt/src/kubevirt/VirtualMachines/Details.tsx

Lines changed: 160 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
22
import { Link, Resource } from '@kinvolk/headlamp-plugin/lib/components/common';
33
import { ActionButton } from '@kinvolk/headlamp-plugin/lib/components/common';
4+
import { Chip } from '@mui/material';
45
import { useSnackbar } from 'notistack';
56
import { useEffect, useState } from 'react';
67
import { useTranslation } from 'react-i18next';
@@ -22,76 +23,131 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
2223
const [showTerminal, setShowTerminal] = useState(false);
2324

2425
const [podName, setPodName] = useState<string | null>(null);
26+
const [nodeName, setNodeName] = useState<string | null>(null);
2527
useEffect(() => {
26-
const fetchPodName = async () => {
28+
const fetchInitial = async () => {
2729
try {
28-
const podName = await getPodName(name, namespace);
29-
setPodName(podName);
30+
const info = await getPodInfo(name, namespace);
31+
setPodName(info.podName);
32+
setNodeName(info.nodeName);
3033
} catch (error) {
31-
console.error('Failed to get pod name', error);
34+
console.error('Failed to get pod info', error);
3235
}
3336
};
3437

35-
fetchPodName();
38+
fetchInitial();
39+
40+
const queryParams = new URLSearchParams();
41+
queryParams.append('labelSelector', `vm.kubevirt.io/name=${name}`);
42+
queryParams.append('watch', 'true');
43+
const url = `/api/v1/namespaces/${namespace}/pods?${queryParams.toString()}`;
44+
45+
const onStream = (result, disconnect, error) => {
46+
if (error) {
47+
console.error('Stream error:', error);
48+
disconnect();
49+
return;
50+
}
51+
52+
const event = result;
53+
if (event.type === 'ADDED' || event.type === 'MODIFIED') {
54+
const pod = event.object;
55+
setPodName(pod.metadata.name);
56+
setNodeName(pod.spec.nodeName || 'Unknown');
57+
} else if (event.type === 'DELETED') {
58+
setPodName('Unknown');
59+
setNodeName('Unknown');
60+
}
61+
};
62+
63+
const { cancel: cancelPod } = ApiProxy.stream(url, onStream, { isJson: true });
64+
return () => {
65+
cancelPod();
66+
};
3667
}, [name, namespace]);
3768
return (
3869
<Resource.DetailsGrid
3970
name={name}
4071
namespace={namespace}
4172
resourceType={VirtualMachine}
73+
withEvents
4274
extraInfo={item =>
43-
item && [
44-
{
45-
name: t('Status'),
46-
value: item?.jsonData.status.printableStatus,
47-
},
48-
{
49-
name: 'VirtualMachineInstance',
50-
value: (
51-
<Link
52-
routeName="/kubevirt/virtualmachinesinstances/:namespace/:name"
53-
params={{ name: item.getName(), namespace: item.getNamespace() }}
54-
>
55-
{item.getName()}
56-
</Link>
57-
),
58-
},
59-
{
60-
name: 'Pod',
61-
value: (
62-
<Link
63-
routeName="pod"
64-
params={{
65-
name: podName,
66-
namespace: item.getNamespace(),
67-
}}
68-
>
69-
{podName}
70-
</Link>
71-
),
72-
},
73-
]
75+
item
76+
? [
77+
{
78+
name: t('Status'),
79+
value: (
80+
<Chip
81+
label={item?.jsonData.status.printableStatus || 'Unknown'}
82+
color={getStatusColor(item?.jsonData.status.printableStatus || 'Unknown')}
83+
variant="outlined"
84+
/>
85+
),
86+
},
87+
{
88+
name: 'VirtualMachineInstance',
89+
value: (
90+
<Link
91+
routeName="/kubevirt/virtualmachinesinstances/:namespace/:name"
92+
params={{ name: item.getName(), namespace: item.getNamespace() }}
93+
>
94+
{item.getName()}
95+
</Link>
96+
),
97+
},
98+
{
99+
name: 'Pod',
100+
value:
101+
podName && podName !== 'Unknown' ? (
102+
<Link
103+
routeName="pod"
104+
params={{
105+
name: podName,
106+
namespace: item.getNamespace(),
107+
}}
108+
>
109+
{podName}
110+
</Link>
111+
) : (
112+
'Unknown'
113+
),
114+
},
115+
{
116+
name: 'Node',
117+
value:
118+
nodeName && nodeName !== 'Unknown' ? (
119+
<Link routeName="node" params={{ name: nodeName }}>
120+
{nodeName}
121+
</Link>
122+
) : (
123+
'Unknown'
124+
),
125+
},
126+
]
127+
: null
74128
}
75129
extraSections={item =>
76-
item && [
77-
{
78-
id: 'status',
79-
section: <Resource.ConditionsSection resource={item?.jsonData} />,
80-
},
81-
{
82-
id: 'headlamp.vm-terminal',
83-
section: (
84-
<Terminal
85-
open={showTerminal}
86-
key="terminal"
87-
item={item}
88-
onClose={() => {
89-
setShowTerminal(false);
90-
}}
91-
/>
92-
),
93-
},
94-
]
130+
item
131+
? [
132+
{
133+
id: 'status',
134+
section: <Resource.ConditionsSection resource={item?.jsonData} />,
135+
},
136+
{
137+
id: 'headlamp.vm-terminal',
138+
section: (
139+
<Terminal
140+
open={showTerminal}
141+
key="terminal"
142+
item={item}
143+
onClose={() => {
144+
setShowTerminal(false);
145+
}}
146+
/>
147+
),
148+
},
149+
]
150+
: null
95151
}
96152
actions={item => {
97153
if (!item) return [];
@@ -112,7 +168,6 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
112168
description={t('Start')}
113169
icon="mdi:play"
114170
onClick={() => {
115-
console.log('Starting ' + item.getName());
116171
item
117172
.start()
118173
.then(() =>
@@ -136,7 +191,6 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
136191
description={t('Stop')}
137192
icon="mdi:stop"
138193
onClick={() => {
139-
console.log('Stopping ' + item.getName());
140194
item
141195
.stop()
142196
.then(() =>
@@ -160,7 +214,6 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
160214
description={t('Pause')}
161215
icon="mdi:pause"
162216
onClick={() => {
163-
console.log('Pausing ' + item.getName());
164217
item
165218
.pause()
166219
.then(() =>
@@ -179,6 +232,29 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
179232
});
180233
}
181234

235+
if (isRunning) {
236+
actionsList.push({
237+
id: 'migrate',
238+
action: (
239+
<ActionButton
240+
description={t('Live Migrate')}
241+
icon="mdi:swap-horizontal"
242+
onClick={() => {
243+
item
244+
.migrate()
245+
.then(() => {
246+
enqueueSnackbar(t('Live migration initiated'), { variant: 'success' });
247+
})
248+
.catch(e => {
249+
console.error('Migration failed', e);
250+
enqueueSnackbar(t('Failed to initiate live migration'), { variant: 'error' });
251+
});
252+
}}
253+
/>
254+
),
255+
});
256+
}
257+
182258
if (isPaused) {
183259
actionsList.push({
184260
id: 'unpause',
@@ -187,7 +263,6 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
187263
description={t('Unpause')}
188264
icon="mdi:play-pause"
189265
onClick={() => {
190-
console.log('Unpausing ' + item.getName());
191266
item
192267
.unpause()
193268
.then(() =>
@@ -227,17 +302,36 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
227302
);
228303
}
229304

230-
async function getPodName(name: string, namespace: string): Promise<string> {
305+
function getStatusColor(status: string) {
306+
if (status === 'Running') return 'success';
307+
if (status === 'Failed') return 'error';
308+
if (['Migrating', 'Starting', 'Stopping'].includes(status)) return 'warning';
309+
return 'default';
310+
}
311+
312+
async function getPodInfo(
313+
name: string,
314+
namespace: string
315+
): Promise<{ podName: string; nodeName: string }> {
231316
const request = ApiProxy.request;
232317
const queryParams = new URLSearchParams();
233-
let response;
234318
queryParams.append('labelSelector', `vm.kubevirt.io/name=${name}`);
235319
try {
236-
response = await request(`/api/v1/namespaces/${namespace}/pods?${queryParams.toString()}`, {
237-
method: 'GET',
238-
});
320+
const response = await request(
321+
`/api/v1/namespaces/${namespace}/pods?${queryParams.toString()}`,
322+
{
323+
method: 'GET',
324+
}
325+
);
326+
const pod = response?.items[0];
327+
if (pod) {
328+
return {
329+
podName: pod.metadata.name,
330+
nodeName: pod.spec.nodeName || 'Unknown',
331+
};
332+
}
333+
return { podName: 'Unknown', nodeName: 'Unknown' };
239334
} catch (error) {
240-
return 'Unknown';
335+
return { podName: 'Unknown', nodeName: 'Unknown' };
241336
}
242-
return response?.items[0]?.metadata?.name || 'Unknown';
243337
}

0 commit comments

Comments
 (0)