feat: allow to use libkrun and applehv machines starting from 5.2.0 (#8247)

* feat: allow to use libkrun and applehv machines starting from 5.2.0

Signed-off-by: Luca Stocchi <luca@MacBook-Pro-di-Luca.local>

* fix: add custom exec function to handle merge of provider and runoptions

Signed-off-by: lstocchi <lstocchi@redhat.com>

* fix: add tests

Signed-off-by: lstocchi <lstocchi@redhat.com>

* fix: remove console log

Signed-off-by: lstocchi <lstocchi@redhat.com>

* fix: use custom label instead of provider name
Signed-off-by: Luca Stocchi <luca@MacBook-Pro-di-Luca.local>

* fix: add tests

Signed-off-by: lstocchi <lstocchi@redhat.com>

---------

Signed-off-by: Luca Stocchi <luca@MacBook-Pro-di-Luca.local>
Signed-off-by: lstocchi <lstocchi@redhat.com>
Co-authored-by: Luca Stocchi <luca@MacBook-Pro-di-Luca.local>
This commit is contained in:
Luca Stocchi 2024-07-29 16:04:13 +02:00 committed by GitHub
parent 9c35fa24e6
commit ffe418e952
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 396 additions and 78 deletions

View file

@ -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,

View file

@ -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<extensionApi.RunResult>((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);

View file

@ -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<void> {
// 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<void
// check if connection is in sync with machine. If the default connection is rootless but the machine is rootful ask the user to update the connection
if (defaultConnectionNotify && !!runningMachine?.Default) {
const defaultConnection = await getDefaultConnection();
const isRootful = await isRootfulMachine(runningMachine.Name);
const isRootful = await isRootfulMachine(runningMachine);
if (!defaultConnection?.Name.endsWith(ROOTFUL_SUFFIX) && isRootful) {
const result = await extensionApi.window.showInformationMessage(
`${isRootful ? 'Rootful' : 'Rootless'} Podman Machine '${runningMachine.Name}' does not match default connection. This will cause podman CLI errors while trying to connect to '${runningMachine.Name}'. Do you want to update the default connection?`,
@ -401,7 +419,7 @@ export async function checkDefaultMachine(machines: MachineJSON[]): Promise<void
try {
const connectionName = isRootful ? `${runningMachine.Name}${ROOTFUL_SUFFIX}` : runningMachine.Name;
// make it the default to run the info command
await extensionApi.process.exec(getPodmanCli(), ['system', 'connection', 'default', connectionName]);
await execPodman(['system', 'connection', 'default', connectionName], runningMachine.VMType);
} catch (error) {
// eslint-disable-next-line quotes
console.error("Error running 'podman system connection default': ", error);
@ -433,12 +451,12 @@ export async function checkDefaultMachine(machines: MachineJSON[]): Promise<void
);
if (result === 'Yes') {
// check if machine is rootless or rootful
const machineIsRootful = await isRootfulMachine(runningMachine.Name);
const machineIsRootful = await isRootfulMachine(runningMachine);
try {
const connectionName = machineIsRootful ? `${runningMachine.Name}${ROOTFUL_SUFFIX}` : runningMachine.Name;
// make it the default to run the info command
await extensionApi.process.exec(getPodmanCli(), ['system', 'connection', 'default', connectionName]);
await execPodman(['system', 'connection', 'default', connectionName], runningMachine.VMType);
} catch (error) {
// eslint-disable-next-line quotes
console.error("Error running 'podman system connection default': ", error);
@ -458,17 +476,16 @@ export async function checkDefaultMachine(machines: MachineJSON[]): Promise<void
}
}
async function isRootfulMachine(machineName: string): Promise<boolean> {
async function isRootfulMachine(machineJSON: MachineJSON): Promise<boolean> {
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<void> => {
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<void> => {
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<string | undefined> {
// 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<void> {
}
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<void> {
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<MachineListOutput> {
const { stdout, stderr } = await extensionApi.process.exec(getPodmanCli(), ['machine', 'list', '--format', 'json']);
export async function getJSONMachineList(): Promise<MachineJSONListOutput> {
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<MachineListOutput> {
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<string, unknown>,
@ -1799,11 +1851,21 @@ export async function createMachine(
const telemetryRecords: Record<string, unknown> = {};
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;

View file

@ -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

View file

@ -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;

View file

@ -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<extensionApi.RunResult>(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<extensionApi.RunResult>(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);
});

View file

@ -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<extensionApi.RunResult> {
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;
}
}

View file

@ -37,6 +37,7 @@ export interface ProviderContainerConnectionInfo {
};
lifecycleMethods?: LifecycleMethod[];
type: 'docker' | 'podman';
vmType?: string;
}
export interface ProviderKubernetesConnectionInfo {

View file

@ -362,6 +362,7 @@ declare module '@podman-desktop/api' {
endpoint: ContainerProviderConnectionEndpoint;
lifecycle?: ProviderConnectionLifecycle;
status(): ProviderConnectionStatus;
vmType?: string;
}
export interface PodCreatePortOptions {

View file

@ -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;

View file

@ -639,6 +639,7 @@ export class ProviderRegistry {
endpoint: {
socketPath: connection.endpoint.socketPath,
},
vmType: connection.vmType,
};
} else {
providerConnection = {

View file

@ -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 () => {

View file

@ -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} />
<div class="mt-1.5 text-gray-900 text-[9px]" aria-label="Connection Version">
<div>{provider.name} {provider.version ? `v${provider.version}` : ''}</div>
<div class="mt-1.5 text-gray-900 text-[9px] flex justify-between">
<div aria-label="Connection Version">
{provider.name}
{provider.version ? `v${provider.version}` : ''}
</div>
<div aria-label="Connection Type">{container.vmType ? capitalize(container.vmType) : ''}</div>
</div>
</div>
{/each}