diff --git a/kubevirt/.eslintcache b/kubevirt/.eslintcache index 62158cf..5cde44f 100644 --- a/kubevirt/.eslintcache +++ b/kubevirt/.eslintcache @@ -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",[],[]] \ No newline at end of file +[{"/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",[],[]] \ No newline at end of file diff --git a/kubevirt/artifacthub-pkg.yml b/kubevirt/artifacthub-pkg.yml index fccab1f..9042652 100644 --- a/kubevirt/artifacthub-pkg.yml +++ b/kubevirt/artifacthub-pkg.yml @@ -1,12 +1,12 @@ -version: v0.0.1-beta5 +version: v0.0.1-beta6 name: buttah_kubevirt displayName: Kubevirt createdAt: "2025-01-01T00:00:00Z" logoURL: "https://github.com/kubevirt/community/blob/main/logo/KubeVirt_icon.png?raw=true" description: "A plugin for managing KubeVirt virtual machines within a Kubernetes cluster." annotations: - headlamp/plugin/archive-url: "https://github.com/buttahtoast/headlamp-plugins/releases/download/kubevirt-v0.0.1-beta5/kubevirt-v0.0.1-beta5.tar.gz" - headlamp/plugin/archive-checksum: "SHA256:77cfd22a90964804b16780f66c627f4e913223f6a6a4b2e0e5efb2b24ba36467" + headlamp/plugin/archive-url: "https://github.com/buttahtoast/headlamp-plugins/releases/download/kubevirt-v0.0.1-beta6/kubevirt-v0.0.1-beta6.tar.gz" + headlamp/plugin/archive-checksum: "SHA256:68a98b4204f182154b55f1ac899461a6112cf1698ba226da4991d44c0453da8a" headlamp/plugin/version-compat: ">=0.24" headlamp/plugin/distro-compat: "in-cluster,web,docker-desktop,desktop" links: diff --git a/kubevirt/package.json b/kubevirt/package.json index a252e57..2e28ae0 100644 --- a/kubevirt/package.json +++ b/kubevirt/package.json @@ -1,6 +1,6 @@ { "name": "kubevirt", - "version": "v0.0.1-beta5", + "version": "v0.0.1-beta6", "description": "A plugin for managing KubeVirt virtual machines within a Kubernetes cluster.", "scripts": { "start": "headlamp-plugin start", diff --git a/kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx b/kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx index bf33ca6..e917cd9 100644 --- a/kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx +++ b/kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx @@ -22,6 +22,7 @@ export default function VirtualMachineInstanceDetails(props: VirtualMachineInsta name={name} namespace={namespace} resourceType={VirtualMachineInstance} + withEvents extraInfo={item => item && [ { diff --git a/kubevirt/src/kubevirt/VirtualMachines/Details.tsx b/kubevirt/src/kubevirt/VirtualMachines/Details.tsx index a026009..50d7d2b 100644 --- a/kubevirt/src/kubevirt/VirtualMachines/Details.tsx +++ b/kubevirt/src/kubevirt/VirtualMachines/Details.tsx @@ -1,6 +1,7 @@ import { ApiProxy } from '@kinvolk/headlamp-plugin/lib'; import { Link, Resource } from '@kinvolk/headlamp-plugin/lib/components/common'; import { ActionButton } from '@kinvolk/headlamp-plugin/lib/components/common'; +import { Chip } from '@mui/material'; import { useSnackbar } from 'notistack'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -22,76 +23,131 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps) const [showTerminal, setShowTerminal] = useState(false); const [podName, setPodName] = useState(null); + const [nodeName, setNodeName] = useState(null); useEffect(() => { - const fetchPodName = async () => { + const fetchInitial = async () => { try { - const podName = await getPodName(name, namespace); - setPodName(podName); + const info = await getPodInfo(name, namespace); + setPodName(info.podName); + setNodeName(info.nodeName); } catch (error) { - console.error('Failed to get pod name', error); + console.error('Failed to get pod info', error); } }; - fetchPodName(); + fetchInitial(); + + const queryParams = new URLSearchParams(); + queryParams.append('labelSelector', `vm.kubevirt.io/name=${name}`); + queryParams.append('watch', 'true'); + const url = `/api/v1/namespaces/${namespace}/pods?${queryParams.toString()}`; + + const onStream = (result, disconnect, error) => { + if (error) { + console.error('Stream error:', error); + disconnect(); + return; + } + + const event = result; + if (event.type === 'ADDED' || event.type === 'MODIFIED') { + const pod = event.object; + setPodName(pod.metadata.name); + setNodeName(pod.spec.nodeName || 'Unknown'); + } else if (event.type === 'DELETED') { + setPodName('Unknown'); + setNodeName('Unknown'); + } + }; + + const { cancel: cancelPod } = ApiProxy.stream(url, onStream, { isJson: true }); + return () => { + cancelPod(); + }; }, [name, namespace]); return ( - item && [ - { - name: t('Status'), - value: item?.jsonData.status.printableStatus, - }, - { - name: 'VirtualMachineInstance', - value: ( - - {item.getName()} - - ), - }, - { - name: 'Pod', - value: ( - - {podName} - - ), - }, - ] + item + ? [ + { + name: t('Status'), + value: ( + + ), + }, + { + name: 'VirtualMachineInstance', + value: ( + + {item.getName()} + + ), + }, + { + name: 'Pod', + value: + podName && podName !== 'Unknown' ? ( + + {podName} + + ) : ( + 'Unknown' + ), + }, + { + name: 'Node', + value: + nodeName && nodeName !== 'Unknown' ? ( + + {nodeName} + + ) : ( + 'Unknown' + ), + }, + ] + : null } extraSections={item => - item && [ - { - id: 'status', - section: , - }, - { - id: 'headlamp.vm-terminal', - section: ( - { - setShowTerminal(false); - }} - /> - ), - }, - ] + item + ? [ + { + id: 'status', + section: , + }, + { + id: 'headlamp.vm-terminal', + section: ( + { + setShowTerminal(false); + }} + /> + ), + }, + ] + : null } actions={item => { if (!item) return []; @@ -112,7 +168,6 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps) description={t('Start')} icon="mdi:play" onClick={() => { - console.log('Starting ' + item.getName()); item .start() .then(() => @@ -136,7 +191,6 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps) description={t('Stop')} icon="mdi:stop" onClick={() => { - console.log('Stopping ' + item.getName()); item .stop() .then(() => @@ -160,7 +214,6 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps) description={t('Pause')} icon="mdi:pause" onClick={() => { - console.log('Pausing ' + item.getName()); item .pause() .then(() => @@ -179,6 +232,29 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps) }); } + if (isRunning) { + actionsList.push({ + id: 'migrate', + action: ( + { + item + .migrate() + .then(() => { + enqueueSnackbar(t('Live migration initiated'), { variant: 'success' }); + }) + .catch(e => { + console.error('Migration failed', e); + enqueueSnackbar(t('Failed to initiate live migration'), { variant: 'error' }); + }); + }} + /> + ), + }); + } + if (isPaused) { actionsList.push({ id: 'unpause', @@ -187,7 +263,6 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps) description={t('Unpause')} icon="mdi:play-pause" onClick={() => { - console.log('Unpausing ' + item.getName()); item .unpause() .then(() => @@ -227,17 +302,36 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps) ); } -async function getPodName(name: string, namespace: string): Promise { +function getStatusColor(status: string) { + if (status === 'Running') return 'success'; + if (status === 'Failed') return 'error'; + if (['Migrating', 'Starting', 'Stopping'].includes(status)) return 'warning'; + return 'default'; +} + +async function getPodInfo( + name: string, + namespace: string +): Promise<{ podName: string; nodeName: string }> { const request = ApiProxy.request; const queryParams = new URLSearchParams(); - let response; queryParams.append('labelSelector', `vm.kubevirt.io/name=${name}`); try { - response = await request(`/api/v1/namespaces/${namespace}/pods?${queryParams.toString()}`, { - method: 'GET', - }); + const response = await request( + `/api/v1/namespaces/${namespace}/pods?${queryParams.toString()}`, + { + method: 'GET', + } + ); + const pod = response?.items[0]; + if (pod) { + return { + podName: pod.metadata.name, + nodeName: pod.spec.nodeName || 'Unknown', + }; + } + return { podName: 'Unknown', nodeName: 'Unknown' }; } catch (error) { - return 'Unknown'; + return { podName: 'Unknown', nodeName: 'Unknown' }; } - return response?.items[0]?.metadata?.name || 'Unknown'; } diff --git a/kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx b/kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx index da87514..977cd91 100644 --- a/kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx +++ b/kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx @@ -1,9 +1,9 @@ -import { StreamArgs, StreamResultsCb } from '@kinvolk/headlamp-plugin/lib/ApiProxy'; +import ApiProxy, { StreamArgs, StreamResultsCb } from '@kinvolk/headlamp-plugin/lib/ApiProxy'; import { KubeObject } from '@kinvolk/headlamp-plugin/lib/K8s/cluster'; import VirtualMachineInstance from '../VirtualMachineInstance/VirtualMachineInstance'; class VirtualMachine extends KubeObject { - constructor(jsonData) { + constructor(jsonData: any) { super(jsonData); } @@ -49,6 +49,33 @@ class VirtualMachine extends KubeObject { return instance.unpause(); } + async migrate() { + const migrationName = `${this.getName()}-migration-${Date.now()}`; + const migration = { + apiVersion: 'kubevirt.io/v1', + kind: 'VirtualMachineInstanceMigration', + metadata: { + name: migrationName, + namespace: this.getNamespace(), + }, + spec: { + vmiName: this.getName(), + }, + }; + + await ApiProxy.request( + `/apis/kubevirt.io/v1/namespaces/${this.getNamespace()}/virtualmachineinstancemigrations`, + { + method: 'POST', + body: JSON.stringify(migration), + headers: { + 'Content-Type': 'application/json', + }, + } + ); + return migrationName; + } + static kind = 'VirtualMachine'; static apiVersion = 'kubevirt.io/v1'; static isNamespaced = true;