diff --git a/extensions/podman/package.json b/extensions/podman/package.json index 4756836ecf8..4234a599035 100644 --- a/extensions/podman/package.json +++ b/extensions/podman/package.json @@ -158,6 +158,17 @@ "markdownDescription": "User mode networking (traffic relayed by a user process). See [documentation](https://docs.podman.io/en/latest/markdown/podman-machine-init.1.html#user-mode-networking).", "when": "podman.isUserModeNetworkingSupported == true" }, + "podman.factory.machine.provider": { + "type": "string", + "default": "default (Apple HyperVisor)", + "enum": [ + "default (Apple HyperVisor)", + "GPU enabled (LibKrun)" + ], + "scope": "ContainerProviderConnectionFactory", + "description": "Provider Type", + "when": "podman.isLibkrunSupported" + }, "podman.factory.machine.now": { "type": "boolean", "default": true, diff --git a/extensions/podman/src/extension.spec.ts b/extensions/podman/src/extension.spec.ts index f60cdf224f1..3ae81b0a301 100644 --- a/extensions/podman/src/extension.spec.ts +++ b/extensions/podman/src/extension.spec.ts @@ -38,7 +38,7 @@ import type { InstalledPodman } from './podman-cli'; import * as podmanCli from './podman-cli'; import { PodmanConfiguration } from './podman-configuration'; import { PodmanInstall } from './podman-install'; -import { getAssetsFolder, isLinux, isMac, isWindows, LoggerDelegator } from './util'; +import { getAssetsFolder, isLinux, isMac, isWindows, LIBKRUN_LABEL, LoggerDelegator, VMTYPE } from './util'; const config: Configuration = { get: () => { @@ -87,6 +87,7 @@ const machineInfo: extension.MachineInfo = { cpuUsage: 0, diskUsage: 0, memoryUsage: 0, + vmType: VMTYPE.LIBKRUN, }; const podmanConfiguration = {} as unknown as PodmanConfiguration; @@ -120,6 +121,7 @@ beforeEach(() => { Running: true, Starting: false, Default: false, + VMType: VMTYPE.LIBKRUN, }, { Name: machine1Name, @@ -129,6 +131,7 @@ beforeEach(() => { Running: false, Starting: false, Default: true, + VMType: VMTYPE.LIBKRUN, }, ]; @@ -300,6 +303,7 @@ test('verify create command called with correct values', async () => { 'podman.factory.machine.image-path': 'path', 'podman.factory.machine.memory': '1048000000', // 1048MB = 999.45MiB 'podman.factory.machine.diskSize': '250000000000', // 250GB = 232.83GiB + 'podman.factory.machine.provider': LIBKRUN_LABEL, }, undefined, ); @@ -309,6 +313,9 @@ test('verify create command called with correct values', async () => { { logger: undefined, token: undefined, + env: { + CONTAINERS_MACHINE_PROVIDER: VMTYPE.LIBKRUN, + }, }, ); @@ -497,6 +504,7 @@ test('checkDefaultMachine: do not prompt if the running machine is already the d Running: true, Starting: false, Default: true, + VMType: VMTYPE.LIBKRUN, }, { Name: 'podman-machine-1', @@ -506,6 +514,7 @@ test('checkDefaultMachine: do not prompt if the running machine is already the d Running: false, Starting: false, Default: false, + VMType: VMTYPE.LIBKRUN, }, ]; @@ -566,6 +575,9 @@ test('if a machine is successfully started it changes its state to started', asy await extension.startMachine(provider, podmanConfiguration, machineInfo); expect(spyExecPromise).toBeCalledWith(podmanCli.getPodmanCli(), ['machine', 'start', 'name'], { + env: { + CONTAINERS_MACHINE_PROVIDER: VMTYPE.LIBKRUN, + }, logger: new LoggerDelegator(), }); @@ -794,13 +806,20 @@ test('test checkDefaultMachine - if user wants to change default machine, check await extension.checkDefaultMachine(fakeMachineJSON); - expect(spyExecPromise).toHaveBeenCalledWith(podmanCli.getPodmanCli(), [ - 'system', - 'connection', - 'default', - `${machineDefaultName}-root`, - ]); - expect(inspectCall).toHaveBeenCalledWith(podmanCli.getPodmanCli(), ['machine', 'inspect', machineDefaultName]); + expect(spyExecPromise).toHaveBeenCalledWith( + podmanCli.getPodmanCli(), + ['system', 'connection', 'default', `${machineDefaultName}-root`], + { + env: { + CONTAINERS_MACHINE_PROVIDER: VMTYPE.LIBKRUN, + }, + }, + ); + expect(inspectCall).toHaveBeenCalledWith(podmanCli.getPodmanCli(), ['machine', 'inspect', machineDefaultName], { + env: { + CONTAINERS_MACHINE_PROVIDER: VMTYPE.LIBKRUN, + }, + }); }); test('test checkDefaultMachine - if user wants to change machine, check that it only change the connection once if it is rootless', async () => { @@ -837,13 +856,20 @@ test('test checkDefaultMachine - if user wants to change machine, check that it await extension.checkDefaultMachine(fakeMachineJSON); - expect(spyExecPromise).toHaveBeenCalledWith(podmanCli.getPodmanCli(), [ - 'system', - 'connection', - 'default', - machineDefaultName, - ]); - expect(inspectCall).toHaveBeenCalledWith(podmanCli.getPodmanCli(), ['machine', 'inspect', machineDefaultName]); + expect(spyExecPromise).toHaveBeenCalledWith( + podmanCli.getPodmanCli(), + ['system', 'connection', 'default', machineDefaultName], + { + env: { + CONTAINERS_MACHINE_PROVIDER: VMTYPE.LIBKRUN, + }, + }, + ); + expect(inspectCall).toHaveBeenCalledWith(podmanCli.getPodmanCli(), ['machine', 'inspect', machineDefaultName], { + env: { + CONTAINERS_MACHINE_PROVIDER: VMTYPE.LIBKRUN, + }, + }); }); test('test checkDefaultMachine - if user wants to change machine, check that it only changes to rootless as machine inspect is not returning Rootful field (old versions of podman)', async () => { @@ -873,13 +899,20 @@ test('test checkDefaultMachine - if user wants to change machine, check that it await extension.checkDefaultMachine(fakeMachineJSON); - expect(spyExecPromise).toHaveBeenCalledWith(podmanCli.getPodmanCli(), [ - 'system', - 'connection', - 'default', - machineDefaultName, - ]); - expect(inspectCall).toHaveBeenCalledWith(podmanCli.getPodmanCli(), ['machine', 'inspect', machineDefaultName]); + expect(spyExecPromise).toHaveBeenCalledWith( + podmanCli.getPodmanCli(), + ['system', 'connection', 'default', machineDefaultName], + { + env: { + CONTAINERS_MACHINE_PROVIDER: VMTYPE.LIBKRUN, + }, + }, + ); + expect(inspectCall).toHaveBeenCalledWith(podmanCli.getPodmanCli(), ['machine', 'inspect', machineDefaultName], { + env: { + CONTAINERS_MACHINE_PROVIDER: VMTYPE.LIBKRUN, + }, + }); }); test('test checkDefaultMachine, if the default connection is not in sync with the default machine, the function will prompt', async () => { @@ -909,6 +942,7 @@ test('test checkDefaultMachine, if the default connection is not in sync with th Running: true, Starting: false, Default: true, + VMType: VMTYPE.LIBKRUN, }, ]; @@ -1170,6 +1204,8 @@ test('ensure showNotification is not called during update', async () => { new Promise((resolve, reject) => { if (args?.[0] === 'machine' && args?.[1] === 'list') { reject(new Error('error')); + } else if (args?.[0] === '--version') { + resolve({} as extensionApi.RunResult); } }), ); @@ -1555,8 +1591,14 @@ describe('registerOnboardingMachineExistsCommand', () => { vi.mocked(extensionApi.commands.registerCommand).mockReturnValue({ dispose: vi.fn() }); - // return 2 empty machines - vi.mocked(extensionApi.process.exec).mockResolvedValue({ stdout: '[{}, {}]' } as unknown as extensionApi.RunResult); + // return an empty object for the first call + vi.spyOn(extensionApi.process, 'exec').mockResolvedValueOnce({ + stdout: 'podman version 5.0.0', + } as extensionApi.RunResult); + // return 2 empty machines for the second call + vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ + stdout: '[{}, {}]', + } as unknown as extensionApi.RunResult); // perform the call const disposable = registerOnboardingMachineExistsCommand(); @@ -1693,7 +1735,11 @@ describe('registerOnboardingUnsupportedPodmanMachineCommand', () => { stdout: 'podman version 4.9.3', } as unknown as extensionApi.RunResult); - // second call to get the machine list + vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ + stdout: 'podman version 4.9.3', + } as unknown as extensionApi.RunResult); + + // third call to get the machine list vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ stdout: '[]', } as unknown as extensionApi.RunResult); @@ -1811,6 +1857,10 @@ describe('registerOnboardingRemoveUnsupportedMachinesCommand', () => { vi.mocked(extensionApi.commands.registerCommand).mockReturnValue({ dispose: vi.fn() }); + vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ + stdout: 'podman version 5.0.0', + } as unknown as extensionApi.RunResult); + vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ stdout: 'podman version 5.0.0', } as unknown as extensionApi.RunResult); @@ -1869,6 +1919,11 @@ describe('registerOnboardingRemoveUnsupportedMachinesCommand', () => { vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ stdout: 'podman version 5.0.0', } as unknown as extensionApi.RunResult); + + vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ + stdout: 'podman version 5.0.0', + } as unknown as extensionApi.RunResult); + // two times false (no qemu folders) vi.mocked(fs.existsSync).mockReturnValueOnce(false); vi.mocked(fs.existsSync).mockReturnValueOnce(false); diff --git a/extensions/podman/src/extension.ts b/extensions/podman/src/extension.ts index 11fa0e1b980..a26e4506324 100644 --- a/extensions/podman/src/extension.ts +++ b/extensions/podman/src/extension.ts @@ -38,7 +38,19 @@ import { PodmanInfoHelper } from './podman-info-helper'; import { PodmanInstall } from './podman-install'; import { QemuHelper } from './qemu-helper'; import { RegistrySetup } from './registry-setup'; -import { appConfigDir, appHomeDir, getAssetsFolder, isLinux, isMac, isWindows, LoggerDelegator } from './util'; +import { + appConfigDir, + appHomeDir, + execPodman, + getAssetsFolder, + getProviderByLabel, + getProviderLabel, + isLinux, + isMac, + isWindows, + LoggerDelegator, + VMTYPE, +} from './util'; import { getDisguisedPodmanInformation, getSocketPath, isDisguisedPodman } from './warnings'; import { WslHelper } from './wsl-helper'; @@ -99,6 +111,7 @@ export type MachineJSON = { Running: boolean; Starting: boolean; Default: boolean; + VMType: string; UserModeNetworking?: boolean; }; @@ -119,6 +132,7 @@ export type MachineInfo = { cpuUsage: number; diskUsage: number; memoryUsage: number; + vmType: string; }; export type MachineListOutput = { @@ -126,6 +140,11 @@ export type MachineListOutput = { stderr: string; }; +export type MachineJSONListOutput = { + list: MachineJSON[]; + error: string; +}; + export function isIncompatibleMachineOutput(output: string | undefined): boolean { // apple HV v4 to v5 machine config error const APPLE_HV_V4_V5_ERROR = 'incompatible machine config'; @@ -152,7 +171,7 @@ export async function updateMachines( podmanConfiguration: PodmanConfiguration, ): Promise { // init machines available - let machineListOutput: MachineListOutput; + let machineListOutput: MachineJSONListOutput; try { machineListOutput = await getJSONMachineList(); } catch (error) { @@ -175,7 +194,7 @@ export async function updateMachines( } // parse output - const machines = JSON.parse(machineListOutput.stdout) as MachineJSON[]; + const machines = machineListOutput.list; extensionApi.context.setValue('podmanMachineExists', machines.length > 0, 'onboarding'); const installedPodman = await getPodmanInstallation(); let shouldCleanMachine = false; @@ -184,7 +203,7 @@ export async function updateMachines( } // check if the machine needs to be cleaned for v4 --> v5 format if (!shouldCleanMachine) { - shouldCleanMachine = isIncompatibleMachineOutput(machineListOutput.stderr); + shouldCleanMachine = isIncompatibleMachineOutput(machineListOutput.error); } // invalid machines is not making the provider working properly so always notify @@ -255,6 +274,7 @@ export async function updateMachines( machineInfo?.memory !== undefined && machineInfo?.memoryUsed !== undefined && machineInfo?.memoryUsed > 0 ? (machineInfo?.memoryUsed * 100) / machineInfo?.memory : 0, + vmType: getProviderLabel(machine.VMType), }); if (!podmanMachinesStatuses.has(machine.Name)) { @@ -297,13 +317,11 @@ export async function updateMachines( ]); socketPath = socket; } else { - const { stdout: socket } = await extensionApi.process.exec(getPodmanCli(), [ - 'machine', - 'inspect', - '--format', - '{{.ConnectionInfo.PodmanSocket.Path}}', - machineName, - ]); + const podmanMachineInfo = podmanMachinesInfo.get(machineName); + const { stdout: socket } = await execPodman( + ['machine', 'inspect', '--format', '{{.ConnectionInfo.PodmanSocket.Path}}', machineName], + podmanMachineInfo?.vmType, + ); socketPath = socket; } } catch (error) { @@ -389,7 +407,7 @@ export async function checkDefaultMachine(machines: MachineJSON[]): Promise { +async function isRootfulMachine(machineJSON: MachineJSON): Promise { let isRootful = false; try { - const { stdout: machineInspectJson } = await extensionApi.process.exec(getPodmanCli(), [ - 'machine', - 'inspect', - machineName, - ]); + const { stdout: machineInspectJson } = await execPodman( + ['machine', 'inspect', machineJSON.Name], + machineJSON.VMType, + ); const machinesInspect = JSON.parse(machineInspectJson); // find the machine name in the array - const machineInspect = machinesInspect.find((machine: { Name: string }) => machine.Name === machineName); + const machineInspect = machinesInspect.find((machine: { Name: string }) => machine.Name === machineJSON.Name); isRootful = machineInspect?.Rootful ?? false; } catch (error) { console.error('Error when checking rootful machine: ', error); @@ -728,13 +745,15 @@ export async function registerProviderFor( await startMachine(provider, podmanConfiguration, machineInfo, context, logger, undefined, false); }, stop: async (context, logger): Promise => { - await extensionApi.process.exec(getPodmanCli(), ['machine', 'stop', machineInfo.name], { + await execPodman(['machine', 'stop', machineInfo.name], machineInfo.vmType, { logger: new LoggerDelegator(context, logger), }); provider.updateStatus('stopped'); }, delete: async (logger): Promise => { - await extensionApi.process.exec(getPodmanCli(), ['machine', 'rm', '-f', machineInfo.name], { logger }); + await execPodman(['machine', 'rm', '-f', machineInfo.name], machineInfo.vmType, { + logger, + }); }, }; //support edit only on MacOS as Podman WSL is nop and generates errors @@ -760,7 +779,7 @@ export async function registerProviderFor( if (state === 'started') { await lifecycle.stop?.(context, logger); } - await extensionApi.process.exec(getPodmanCli(), args, { + await execPodman(args, machineInfo.vmType, { logger: new LoggerDelegator(context, logger), }); } finally { @@ -780,6 +799,7 @@ export async function registerProviderFor( endpoint: { socketPath, }, + vmType: getProviderLabel(machineInfo.vmType), }; // Since Podman 4.5, machines are using the same path for all sockets of machines @@ -854,7 +874,7 @@ export async function startMachine( try { // start the machine - await extensionApi.process.exec(getPodmanCli(), ['machine', 'start', machineInfo.name], { + await execPodman(['machine', 'start', machineInfo.name], machineInfo.vmType, { logger: new LoggerDelegator(context, logger), }); provider.updateStatus('started'); @@ -967,6 +987,7 @@ export const CLEANUP_REQUIRED_MACHINE_KEY = 'podman.needPodmanMachineCleanup'; export const PODMAN_MACHINE_CPU_SUPPORTED_KEY = 'podman.podmanMachineCpuSupported'; export const PODMAN_MACHINE_MEMORY_SUPPORTED_KEY = 'podman.podmanMachineMemorySupported'; export const PODMAN_MACHINE_DISK_SUPPORTED_KEY = 'podman.podmanMachineDiskSupported'; +export const PODMAN_PROVIDER_LIBKRUN_SUPPORTED_KEY = 'podman.isLibkrunSupported'; export function initTelemetryLogger(): void { telemetryLogger = extensionApi.env.createTelemetryLogger(); @@ -1026,8 +1047,7 @@ export function registerOnboardingMachineExistsCommand(): extensionApi.Disposabl let machineLength; try { const machineListOutput = await getJSONMachineList(); - const machines = JSON.parse(machineListOutput.stdout) as MachineJSON[]; - machineLength = machines.length; + machineLength = machineListOutput.list.length; } catch (error) { machineLength = 0; } @@ -1047,7 +1067,7 @@ export function registerOnboardingUnsupportedPodmanMachineCommand(): extensionAp if (!isUnsupported) { try { const machineListOutput = await getJSONMachineList(); - isUnsupported = isIncompatibleMachineOutput(machineListOutput.stderr); + isUnsupported = isIncompatibleMachineOutput(machineListOutput.error); } catch (error) { // check if stderr in the error object const runError = error as RunError; @@ -1098,7 +1118,7 @@ export function registerOnboardingRemoveUnsupportedMachinesCommand(): extensionA let machineListError = ''; try { const machineListOutput = await getJSONMachineList(); - machineListError = machineListOutput.stderr; + machineListError = machineListOutput.error; } catch (error) { machineListError = (error as RunError).stderr; } @@ -1117,10 +1137,11 @@ export function registerOnboardingRemoveUnsupportedMachinesCommand(): extensionA const files = await fs.promises.readdir(machineFolderToCheck); const machineFilesToAnalyze = files.filter(file => file.endsWith('.json')); let machineConfigJson: { GvProxy?: string } = {}; + const machineFolderToCheckValue = machineFolderToCheck; const allMachines = await Promise.all( machineFilesToAnalyze.map(async file => { // read content of the file - const absoluteFile = path.join(machineFolderToCheck, file); + const absoluteFile = path.join(machineFolderToCheckValue, file); try { const machineConfigJsonRaw = await fs.promises.readFile(absoluteFile, 'utf-8'); machineConfigJson = JSON.parse(machineConfigJsonRaw); @@ -1215,6 +1236,7 @@ export async function activate(extensionContext: extensionApi.ExtensionContext): extensionApi.context.setValue(ROOTFUL_MACHINE_INIT_SUPPORTED_KEY, isRootfulMachineInitSupported(version)); extensionApi.context.setValue(START_NOW_MACHINE_INIT_SUPPORTED_KEY, isStartNowAtMachineInitSupported(version)); extensionApi.context.setValue(USER_MODE_NETWORKING_SUPPORTED_KEY, isUserModeNetworkingSupported(version)); + extensionApi.context.setValue(PODMAN_PROVIDER_LIBKRUN_SUPPORTED_KEY, isLibkrunSupported(version)); isMovedPodmanSocket = isPodmanSocketLocationMoved(version); } @@ -1635,7 +1657,7 @@ export async function findRunningMachine(): Promise { // Find the machines const machineListOutput = await getJSONMachineList(); - const machines = JSON.parse(machineListOutput.stdout) as MachineJSON[]; + const machines = machineListOutput.list; // Find the machine that is running const found: MachineJSON | undefined = machines.find(machine => machine?.Running); @@ -1654,7 +1676,7 @@ async function stopAutoStartedMachine(): Promise { } const machineListOutput = await getJSONMachineList(); - const machines = JSON.parse(machineListOutput.stdout) as MachineJSON[]; + const machines = machineListOutput.list; // Find the autostarted machine and check its status const currentMachine: MachineJSON | undefined = machines.find(machine => machine?.Name === autoMachineName); @@ -1665,11 +1687,34 @@ async function stopAutoStartedMachine(): Promise { return; } console.log('stopping autostarted machine', autoMachineName); - await extensionApi.process.exec(getPodmanCli(), ['machine', 'stop', autoMachineName]); + await execPodman(['machine', 'stop', autoMachineName], currentMachine.VMType); } -export async function getJSONMachineList(): Promise { - const { stdout, stderr } = await extensionApi.process.exec(getPodmanCli(), ['machine', 'list', '--format', 'json']); +export async function getJSONMachineList(): Promise { + const installedPodman = await getPodmanInstallation(); + + const containerMachineProviders: (string | undefined)[] = []; + // if libkrun is supported we want to show both applehv and libkrun machines + if (installedPodman && isLibkrunSupported(installedPodman.version)) { + containerMachineProviders.push(...['applehv', 'libkrun']); + } else { + // in all other cases we set undefined so that it executes normally by using the default container provider + containerMachineProviders.push(undefined); + } + + const list: MachineJSON[] = []; + let error = ''; + for (const provider of containerMachineProviders) { + const machineListOutput = await getJSONMachineListByProvider(provider); + list.push(...(JSON.parse(machineListOutput.stdout) as MachineJSON[])); + error += machineListOutput.stderr + '\n'; + } + + return { list, error }; +} + +export async function getJSONMachineListByProvider(containerMachineProvider?: string): Promise { + const { stdout, stderr } = await execPodman(['machine', 'list', '--format', 'json'], containerMachineProvider); return { stdout, stderr }; } @@ -1710,6 +1755,13 @@ export function isUserModeNetworkingSupported(podmanVersion: string): boolean { return isWindows() && compareVersions(podmanVersion, PODMAN_MINIMUM_VERSION_FOR_USER_MODE_NETWORKING) >= 0; } +const PODMAN_MINIMUM_VERSION_FOR_LIBKRUN_SUPPORT = '5.2.0'; + +// Checks if libkrun is supported. Only Mac platform allows this parameter to be tuned +export function isLibkrunSupported(podmanVersion: string): boolean { + return isMac() && compareVersions(podmanVersion, PODMAN_MINIMUM_VERSION_FOR_LIBKRUN_SUPPORT) >= 0; +} + function sendTelemetryRecords( eventName: string, telemetryRecords: Record, @@ -1799,11 +1851,21 @@ export async function createMachine( const telemetryRecords: Record = {}; + let provider: string | undefined; + if (params['podman.factory.machine.provider']) { + provider = getProviderByLabel(params['podman.factory.machine.provider']); + } + // cpus if (params['podman.factory.machine.cpus']) { + let cpusValue = params['podman.factory.machine.cpus']; + // libkrun has an issue that prevent to start a machine that has been created with more than 8 cpus, so we limit it here + if (provider === VMTYPE.LIBKRUN && parseInt(cpusValue) > 8) { + cpusValue = '8'; + } parameters.push('--cpus'); - parameters.push(params['podman.factory.machine.cpus']); - telemetryRecords.cpus = params['podman.factory.machine.cpus']; + parameters.push(cpusValue); + telemetryRecords.cpus = cpusValue; } // memory @@ -1896,7 +1958,10 @@ export async function createMachine( const startTime = performance.now(); try { - await extensionApi.process.exec(getPodmanCli(), parameters, { logger, token }); + await execPodman(parameters, provider, { + logger, + token, + }); } catch (error) { telemetryRecords.error = error; const runError = error as RunError; diff --git a/extensions/podman/src/podman-install.spec.ts b/extensions/podman/src/podman-install.spec.ts index 95911347043..39e7246f8d3 100644 --- a/extensions/podman/src/podman-install.spec.ts +++ b/extensions/podman/src/podman-install.spec.ts @@ -26,6 +26,7 @@ import type { InstalledPodman } from './podman-cli'; import type { Installer, UpdateCheck } from './podman-install'; import { getBundledPodmanVersion, PodmanInstall, WinInstaller } from './podman-install'; import * as podmanInstallObj from './podman-install'; +import * as utils from './util'; const originalConsoleError = console.error; const consoleErrorMock = vi.fn(); @@ -85,6 +86,7 @@ vi.mock('./util', async () => { isLinux: vi.fn(), isWindows: vi.fn(), isMac: vi.fn(), + execPodman: vi.fn(), }; }); @@ -729,9 +731,13 @@ describe('update checks', () => { test('stopPodmanMachinesIfAnyBeforeUpdating with one machine running', async () => { const podmanInstall = new TestPodmanInstall(extensionContext); + vi.spyOn(extensionApi.process, 'exec').mockResolvedValueOnce({ + stdout: 'podman version 5.0.0', + } as extensionApi.RunResult); + // return empty machine list - vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ - stdout: JSON.stringify([{ Name: 'test', Running: true }]), + vi.spyOn(utils, 'execPodman').mockResolvedValueOnce({ + stdout: JSON.stringify([{ Name: 'test', Running: true, VMType: 'libkrun' }]), } as unknown as extensionApi.RunResult); // mock user response diff --git a/extensions/podman/src/podman-install.ts b/extensions/podman/src/podman-install.ts index b76e0807894..1ad6784483c 100755 --- a/extensions/podman/src/podman-install.ts +++ b/extensions/podman/src/podman-install.ts @@ -28,9 +28,11 @@ import { getDetectionChecks } from './detection-checks'; import type { MachineJSON } from './extension'; import { getJSONMachineList, + isLibkrunSupported, isRootfulMachineInitSupported, isStartNowAtMachineInitSupported, isUserModeNetworkingSupported, + PODMAN_PROVIDER_LIBKRUN_SUPPORTED_KEY, ROOTFUL_MACHINE_INIT_SUPPORTED_KEY, START_NOW_MACHINE_INIT_SUPPORTED_KEY, USER_MODE_NETWORKING_SUPPORTED_KEY, @@ -174,6 +176,10 @@ export class PodmanInstall { START_NOW_MACHINE_INIT_SUPPORTED_KEY, isStartNowAtMachineInitSupported(newInstalledPodman.version), ); + extensionApi.context.setValue( + PODMAN_PROVIDER_LIBKRUN_SUPPORTED_KEY, + isLibkrunSupported(newInstalledPodman.version), + ); } // update detections checks provider.updateDetectionChecks(getDetectionChecks(newInstalledPodman)); @@ -209,9 +215,7 @@ export class PodmanInstall { const machinesRunning: MachineJSON[] = []; try { const machineListOutput = await getJSONMachineList(); - const machines = JSON.parse(machineListOutput.stdout) as MachineJSON[]; - - machinesRunning.push(...machines.filter(machine => machine.Running || machine.Starting)); + machinesRunning.push(...machineListOutput.list.filter(machine => machine.Running || machine.Starting)); } catch (error) { console.debug('Unable to query machines before updating', error); } @@ -356,6 +360,10 @@ export class PodmanInstall { START_NOW_MACHINE_INIT_SUPPORTED_KEY, isStartNowAtMachineInitSupported(updateInfo.bundledVersion), ); + extensionApi.context.setValue( + PODMAN_PROVIDER_LIBKRUN_SUPPORTED_KEY, + isLibkrunSupported(updateInfo.bundledVersion), + ); } } else if (answer === 'Ignore') { this.podmanInfo.ignoreVersionUpdate = updateInfo.bundledVersion; diff --git a/extensions/podman/src/util.spec.ts b/extensions/podman/src/util.spec.ts index fe9b5faf0a7..2eba1f05dbd 100644 --- a/extensions/podman/src/util.spec.ts +++ b/extensions/podman/src/util.spec.ts @@ -16,9 +16,43 @@ * SPDX-License-Identifier: Apache-2.0 ***********************************************************************/ -import { expect, test } from 'vitest'; +import * as extensionApi from '@podman-desktop/api'; +import { afterEach, expect, test, vi } from 'vitest'; -import { normalizeWSLOutput } from './util'; +import { getPodmanCli } from './podman-cli'; +import { + APPLEHV_LABEL, + execPodman, + getProviderByLabel, + getProviderLabel, + LIBKRUN_LABEL, + normalizeWSLOutput, + VMTYPE, +} from './util'; + +const config: extensionApi.Configuration = { + get: () => { + // not implemented + }, + has: () => true, + update: vi.fn(), +}; + +vi.mock('@podman-desktop/api', () => { + return { + configuration: { + getConfiguration: (): extensionApi.Configuration => config, + }, + process: { + exec: vi.fn(), + }, + }; +}); + +afterEach(() => { + vi.resetAllMocks(); + vi.restoreAllMocks(); +}); test('normalizeWSLOutput returns the same string if there is no need to normalize it', async () => { const text = 'blabla'; @@ -46,3 +80,76 @@ function strEncodeUTF16(str: string): Uint16Array { } return bufView; } + +test('expect exec called with CONTAINERS_MACHINE_PROVIDER if a provider is defined', async () => { + const execMock = vi.spyOn(extensionApi.process, 'exec').mockImplementation( + () => + new Promise(resolve => { + resolve({} as extensionApi.RunResult); + }), + ); + + await execPodman(['machine', 'inspect'], 'libkrun', { + env: { + label: 'one', + }, + }); + + expect(execMock).toBeCalledWith(getPodmanCli(), ['machine', 'inspect'], { + env: { + label: 'one', + CONTAINERS_MACHINE_PROVIDER: 'libkrun', + }, + }); +}); + +test('expect exec called without CONTAINERS_MACHINE_PROVIDER if a provider is NOT defined', async () => { + const execMock = vi.spyOn(extensionApi.process, 'exec').mockImplementation( + () => + new Promise(resolve => { + resolve({} as extensionApi.RunResult); + }), + ); + + await execPodman(['machine', 'inspect'], undefined, { + env: { + label: 'one', + }, + }); + + expect(execMock).toBeCalledWith(getPodmanCli(), ['machine', 'inspect'], { + env: { + label: 'one', + }, + }); +}); + +test('expect libkrun label with libkrun provider', async () => { + const label = getProviderLabel(VMTYPE.LIBKRUN); + expect(label).equals(LIBKRUN_LABEL); +}); + +test('expect applehv label with applehv provider', async () => { + const label = getProviderLabel(VMTYPE.APPLEHV); + expect(label).equals(APPLEHV_LABEL); +}); + +test('expect provider name with provider different from libkrun and applehv', async () => { + const label = getProviderLabel(VMTYPE.WSL); + expect(label).equals(VMTYPE.WSL); +}); + +test('expect libkrun provider with libkrun label', async () => { + const provider = getProviderByLabel(LIBKRUN_LABEL); + expect(provider).equals(VMTYPE.LIBKRUN); +}); + +test('expect applehv provider with applehv label', async () => { + const provider = getProviderByLabel(APPLEHV_LABEL); + expect(provider).equals(VMTYPE.APPLEHV); +}); + +test('expect provider name with provider different from libkrun and applehv', async () => { + const provider = getProviderByLabel(VMTYPE.WSL); + expect(provider).equals(VMTYPE.WSL); +}); diff --git a/extensions/podman/src/util.ts b/extensions/podman/src/util.ts index a7fc739dd67..b1d73bebc93 100644 --- a/extensions/podman/src/util.ts +++ b/extensions/podman/src/util.ts @@ -19,7 +19,9 @@ import * as os from 'node:os'; import * as path from 'node:path'; -import type { LifecycleContext, Logger } from '@podman-desktop/api'; +import * as extensionApi from '@podman-desktop/api'; + +import { getPodmanCli } from './podman-cli'; const windows = os.platform() === 'win32'; export function isWindows(): boolean { @@ -83,10 +85,10 @@ export function getAssetsFolder(): string { * field can be used directly, simplifying the process of passing the logger to the process API while preserving * the necessary functionalities. */ -export class LoggerDelegator implements Logger { - private loggers: Logger[] = []; +export class LoggerDelegator implements extensionApi.Logger { + private loggers: extensionApi.Logger[] = []; - constructor(...loggersOrContexts: (Logger | LifecycleContext | undefined)[]) { + constructor(...loggersOrContexts: (extensionApi.Logger | extensionApi.LifecycleContext | undefined)[]) { loggersOrContexts.forEach(loggerOrContext => { if (loggerOrContext === undefined) { return; @@ -94,7 +96,7 @@ export class LoggerDelegator implements Logger { if (typeof loggerOrContext.log === 'object') { this.loggers.push(loggerOrContext.log); } else if (typeof loggerOrContext.log === 'function') { - this.loggers.push(loggerOrContext as Logger); + this.loggers.push(loggerOrContext as extensionApi.Logger); } }); } @@ -132,3 +134,52 @@ export function normalizeWSLOutput(out: string): string { } return str; } + +export function execPodman( + args: string[], + containersProvider?: string, + options?: extensionApi.RunOptions, +): Promise { + const finalOptions: extensionApi.RunOptions = { ...options }; + + if (containersProvider) { + finalOptions.env = { + ...(finalOptions.env ?? {}), + CONTAINERS_MACHINE_PROVIDER: getProviderByLabel(containersProvider), + }; + } + + return extensionApi.process.exec(getPodmanCli(), args, finalOptions); +} + +export enum VMTYPE { + WSL = 'wsl', + HYPERV = 'hyperv', + APPLEHV = 'applehv', + LIBKRUN = 'libkrun', +} + +export const APPLEHV_LABEL = 'default (Apple HyperVisor)'; +export const LIBKRUN_LABEL = 'GPU enabled (LibKrun)'; + +export function getProviderLabel(provider: string): string { + switch (provider) { + case VMTYPE.LIBKRUN: + return LIBKRUN_LABEL; + case VMTYPE.APPLEHV: + return APPLEHV_LABEL; + default: + return provider; + } +} + +export function getProviderByLabel(label: string): string { + switch (label) { + case LIBKRUN_LABEL: + return VMTYPE.LIBKRUN; + case APPLEHV_LABEL: + return VMTYPE.APPLEHV; + default: + return label; + } +} diff --git a/packages/api/src/provider-info.ts b/packages/api/src/provider-info.ts index 5c434fcb343..91f3a754019 100644 --- a/packages/api/src/provider-info.ts +++ b/packages/api/src/provider-info.ts @@ -37,6 +37,7 @@ export interface ProviderContainerConnectionInfo { }; lifecycleMethods?: LifecycleMethod[]; type: 'docker' | 'podman'; + vmType?: string; } export interface ProviderKubernetesConnectionInfo { diff --git a/packages/extension-api/src/extension-api.d.ts b/packages/extension-api/src/extension-api.d.ts index ba2c6a3386e..add60f493c6 100644 --- a/packages/extension-api/src/extension-api.d.ts +++ b/packages/extension-api/src/extension-api.d.ts @@ -362,6 +362,7 @@ declare module '@podman-desktop/api' { endpoint: ContainerProviderConnectionEndpoint; lifecycle?: ProviderConnectionLifecycle; status(): ProviderConnectionStatus; + vmType?: string; } export interface PodCreatePortOptions { diff --git a/packages/main/src/plugin/provider-registry.spec.ts b/packages/main/src/plugin/provider-registry.spec.ts index 70c3870db45..7a2c6475010 100644 --- a/packages/main/src/plugin/provider-registry.spec.ts +++ b/packages/main/src/plugin/provider-registry.spec.ts @@ -297,6 +297,7 @@ test('should send events when starting a container connection', async () => { socketPath: '/endpoint1.sock', }, status: 'started', + vmType: 'libkrun', }; const startMock = vi.fn(); @@ -314,6 +315,7 @@ test('should send events when starting a container connection', async () => { status() { return 'started'; }, + vmType: 'libkrun', }); let onBeforeDidUpdateContainerConnectionCalled = false; @@ -360,6 +362,7 @@ test('should send events when stopping a container connection', async () => { socketPath: '/endpoint1.sock', }, status: 'stopped', + vmType: 'libkrun', }; const startMock = vi.fn(); @@ -377,6 +380,7 @@ test('should send events when stopping a container connection', async () => { status() { return 'stopped'; }, + vmType: 'libkrun', }); let onBeforeDidUpdateContainerConnectionCalled = false; diff --git a/packages/main/src/plugin/provider-registry.ts b/packages/main/src/plugin/provider-registry.ts index a82896866a0..946a9f4a6de 100644 --- a/packages/main/src/plugin/provider-registry.ts +++ b/packages/main/src/plugin/provider-registry.ts @@ -639,6 +639,7 @@ export class ProviderRegistry { endpoint: { socketPath: connection.endpoint.socketPath, }, + vmType: connection.vmType, }; } else { providerConnection = { diff --git a/packages/renderer/src/lib/preferences/PreferencesResourcesRendering.spec.ts b/packages/renderer/src/lib/preferences/PreferencesResourcesRendering.spec.ts index 4d8873a9f46..188deb4e35d 100644 --- a/packages/renderer/src/lib/preferences/PreferencesResourcesRendering.spec.ts +++ b/packages/renderer/src/lib/preferences/PreferencesResourcesRendering.spec.ts @@ -52,6 +52,7 @@ const providerInfo: ProviderInfo = { }, lifecycleMethods: ['start', 'stop', 'delete'], type: 'podman', + vmType: 'libkrun', }, ], installationSupport: false, @@ -167,6 +168,8 @@ test('Expect type to be reported for Podman engines', async () => { expect(typeDiv.textContent).toBe('Podman endpoint'); const endpointSpan = await vi.waitFor(() => screen.getByTitle('unix://socket')); expect(endpointSpan.textContent).toBe('unix://socket'); + const connectionType = screen.getByLabelText('Connection Type'); + expect(connectionType.textContent).equal('Libkrun'); }); test('Expect type to be reported for Docker engines', async () => { diff --git a/packages/renderer/src/lib/preferences/PreferencesResourcesRendering.svelte b/packages/renderer/src/lib/preferences/PreferencesResourcesRendering.svelte index 73f7314146d..95e113dc62f 100644 --- a/packages/renderer/src/lib/preferences/PreferencesResourcesRendering.svelte +++ b/packages/renderer/src/lib/preferences/PreferencesResourcesRendering.svelte @@ -28,6 +28,7 @@ import { normalizeOnboardingWhenClause } from '../onboarding/onboarding-utils'; import ConnectionErrorInfoButton from '../ui/ConnectionErrorInfoButton.svelte'; import ConnectionStatus from '../ui/ConnectionStatus.svelte'; import EngineIcon from '../ui/EngineIcon.svelte'; +import { capitalize } from '../ui/Util'; import { PeerProperties } from './PeerProperties'; import { eventCollect } from './preferences-connection-rendering-task'; import PreferencesConnectionActions from './PreferencesConnectionActions.svelte'; @@ -530,8 +531,12 @@ function hasAnyConfiguration(provider: ProviderInfo) { connectionStatus={containerConnectionStatus.get(getProviderConnectionName(provider, container))} updateConnectionStatus={updateContainerStatus} addConnectionToRestartingQueue={addConnectionToRestartingQueue} /> -
-
{provider.name} {provider.version ? `v${provider.version}` : ''}
+
+
+ {provider.name} + {provider.version ? `v${provider.version}` : ''} +
+
{container.vmType ? capitalize(container.vmType) : ''}
{/each}