Skip to content

Commit 8ce73bc

Browse files
author
Kevin Klopfenstein
committed
feat: #18 implement vm migrating feature
1 parent 8e9301e commit 8ce73bc

File tree

3 files changed

+195
-65
lines changed

3 files changed

+195
-65
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/src/kubevirt/VirtualMachines/Details.tsx

Lines changed: 161 additions & 62 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 [];
@@ -179,6 +235,30 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
179235
});
180236
}
181237

238+
if (isRunning) {
239+
actionsList.push({
240+
id: 'migrate',
241+
action: (
242+
<ActionButton
243+
description={t('Live Migrate')}
244+
icon="mdi:swap-horizontal"
245+
onClick={() => {
246+
console.log('Starting live migration for ' + item.getName());
247+
item
248+
.migrate()
249+
.then(() => {
250+
enqueueSnackbar(t('Live migration initiated'), { variant: 'success' });
251+
})
252+
.catch(e => {
253+
console.error('Migration failed', e);
254+
enqueueSnackbar(t('Failed to initiate live migration'), { variant: 'error' });
255+
});
256+
}}
257+
/>
258+
),
259+
});
260+
}
261+
182262
if (isPaused) {
183263
actionsList.push({
184264
id: 'unpause',
@@ -227,17 +307,36 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
227307
);
228308
}
229309

230-
async function getPodName(name: string, namespace: string): Promise<string> {
310+
function getStatusColor(status: string) {
311+
if (status === 'Running') return 'success';
312+
if (status === 'Failed') return 'error';
313+
if (['Migrating', 'Starting', 'Stopping'].includes(status)) return 'warning';
314+
return 'default';
315+
}
316+
317+
async function getPodInfo(
318+
name: string,
319+
namespace: string
320+
): Promise<{ podName: string; nodeName: string }> {
231321
const request = ApiProxy.request;
232322
const queryParams = new URLSearchParams();
233-
let response;
234323
queryParams.append('labelSelector', `vm.kubevirt.io/name=${name}`);
235324
try {
236-
response = await request(`/api/v1/namespaces/${namespace}/pods?${queryParams.toString()}`, {
237-
method: 'GET',
238-
});
325+
const response = await request(
326+
`/api/v1/namespaces/${namespace}/pods?${queryParams.toString()}`,
327+
{
328+
method: 'GET',
329+
}
330+
);
331+
const pod = response?.items[0];
332+
if (pod) {
333+
return {
334+
podName: pod.metadata.name,
335+
nodeName: pod.spec.nodeName || 'Unknown',
336+
};
337+
}
338+
return { podName: 'Unknown', nodeName: 'Unknown' };
239339
} catch (error) {
240-
return 'Unknown';
340+
return { podName: 'Unknown', nodeName: 'Unknown' };
241341
}
242-
return response?.items[0]?.metadata?.name || 'Unknown';
243342
}

kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { StreamArgs, StreamResultsCb } from '@kinvolk/headlamp-plugin/lib/ApiProxy';
1+
import ApiProxy, { StreamArgs, StreamResultsCb } from '@kinvolk/headlamp-plugin/lib/ApiProxy';
22
import { KubeObject } from '@kinvolk/headlamp-plugin/lib/K8s/cluster';
33
import VirtualMachineInstance from '../VirtualMachineInstance/VirtualMachineInstance';
44

55
class VirtualMachine extends KubeObject {
6-
constructor(jsonData) {
6+
constructor(jsonData: any) {
77
super(jsonData);
88
}
99

@@ -49,6 +49,37 @@ class VirtualMachine extends KubeObject {
4949
return instance.unpause();
5050
}
5151

52+
async migrate() {
53+
const migrationName = `${this.getName()}-migration-${Date.now()}`;
54+
const migration = {
55+
apiVersion: 'kubevirt.io/v1',
56+
kind: 'VirtualMachineInstanceMigration',
57+
metadata: {
58+
name: migrationName,
59+
namespace: this.getNamespace(),
60+
},
61+
spec: {
62+
vmiName: this.getName(),
63+
},
64+
};
65+
66+
try {
67+
await ApiProxy.request(
68+
`/apis/kubevirt.io/v1/namespaces/${this.getNamespace()}/virtualmachineinstancemigrations`,
69+
{
70+
method: 'POST',
71+
body: JSON.stringify(migration),
72+
headers: {
73+
'Content-Type': 'application/json',
74+
},
75+
}
76+
);
77+
return migrationName;
78+
} catch (error) {
79+
throw error;
80+
}
81+
}
82+
5283
static kind = 'VirtualMachine';
5384
static apiVersion = 'kubevirt.io/v1';
5485
static isNamespaced = true;

0 commit comments

Comments
 (0)