From e6cd9a7ba6e582570931bab2c2f39908f56e21ae Mon Sep 17 00:00:00 2001 From: Luca Stocchi <49404737+lstocchi@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:46:23 +0200 Subject: [PATCH] feat: handle both hyperv and wsl machines (#8985) Signed-off-by: lstocchi --- .../packages/extension/src/extension.spec.ts | 294 +++++++++++++++++- .../packages/extension/src/extension.ts | 32 +- .../extension/src/podman-install.spec.ts | 31 +- .../packages/extension/src/podman-install.ts | 36 ++- 4 files changed, 363 insertions(+), 30 deletions(-) diff --git a/extensions/podman/packages/extension/src/extension.spec.ts b/extensions/podman/packages/extension/src/extension.spec.ts index 087c0baec5ad5..e2732f2288157 100644 --- a/extensions/podman/packages/extension/src/extension.spec.ts +++ b/extensions/podman/packages/extension/src/extension.spec.ts @@ -40,6 +40,7 @@ import { PodmanConfiguration } from './podman-configuration'; import type { UpdateCheck } from './podman-install'; import { PodmanInstall } from './podman-install'; import { getAssetsFolder, isLinux, isMac, isWindows, LIBKRUN_LABEL, LoggerDelegator, VMTYPE } from './util'; +import * as util from './util'; const config: Configuration = { get: () => { @@ -284,21 +285,6 @@ vi.mock('./podman-info-helper', async () => { }), }; }); -vi.mock('./wsl-helper', async () => { - return { - WslHelper: vi.fn().mockImplementation(() => { - return { - getWSLVersionData: vi.fn().mockImplementation(() => { - return Promise.resolve({ - wslVersion: '1.2.3', - kernelVersion: '1.2.3', - windowsVersion: '1.2.3', - }); - }), - }; - }), - }; -}); vi.mock('./util', async () => { // eslint-disable-next-line @typescript-eslint/consistent-type-imports @@ -1947,6 +1933,36 @@ describe('registerOnboardingRemoveUnsupportedMachinesCommand', () => { stdout: 'podman version 5.0.0', } as unknown as extensionApi.RunResult); + vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ + stdout: 'unknown message: 1.2.5.0', + stderr: '', + command: 'command', + }); + + vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ + stdout: 'True', + stderr: '', + command: 'command', + }); + + vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ + stdout: 'True', + stderr: '', + command: 'command', + }); + + vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ + stdout: 'True', + stderr: '', + command: 'command', + }); + + vi.mocked(extensionApi.process.exec).mockResolvedValueOnce({ + stdout: 'Running', + stderr: '', + command: 'command', + }); + // two times false (no qemu folders) vi.mocked(fs.existsSync).mockReturnValueOnce(false); vi.mocked(fs.existsSync).mockReturnValueOnce(false); @@ -2337,3 +2353,251 @@ test('activate function returns an api implementation', async () => { expect(api).toBeDefined(); expect(typeof api.exec).toBe('function'); }); + +test('isHypervEnabled should return false if it is not windows', async () => { + vi.mocked(isWindows).mockReturnValue(false); + const hypervEnabled = await extension.isHyperVEnabled(); + expect(hypervEnabled).toBeFalsy(); +}); + +test('isHypervEnabled should return false if hyperv is not enabled', async () => { + vi.mocked(isWindows).mockReturnValue(true); + const hypervEnabled = await extension.isHyperVEnabled(); + expect(hypervEnabled).toBeFalsy(); +}); + +test('isHypervEnabled should return true if hyperv is enabled', async () => { + vi.mocked(isWindows).mockReturnValue(true); + vi.spyOn(extensionApi.process, 'exec').mockImplementation((command, args) => { + return new Promise(resolve => { + if (command === 'powershell.exe') { + resolve({ + stdout: args?.[0] === '@(Get-Service vmms).Status' ? 'Running' : 'True', + stderr: '', + command: 'command', + }); + } + }); + }); + const wslHypervEnabled = await extension.isHyperVEnabled(); + expect(wslHypervEnabled).toBeTruthy(); +}); + +test('isWSLEnabled should return false if it is not windows', async () => { + vi.mocked(isWindows).mockReturnValue(false); + const wslEnabled = await extension.isWSLEnabled(); + expect(wslEnabled).toBeFalsy(); +}); + +test('isWSLEnabled should return false if wsl is not enabled', async () => { + vi.mocked(isWindows).mockReturnValue(true); + vi.spyOn(extensionApi.process, 'exec').mockResolvedValue({ + stdout: 'unknown message: 1.2.5.0', + stderr: '', + command: 'command', + }); + const wslEnabled = await extension.isWSLEnabled(); + expect(wslEnabled).toBeFalsy(); +}); + +test('isWSLEnabled should return true if wsl is enabled', async () => { + vi.mocked(isWindows).mockReturnValue(true); + vi.spyOn(extensionApi.process, 'exec').mockImplementation(command => { + return new Promise(resolve => { + if (command === 'wsl') { + resolve({ + stdout: + 'WSL version: 2.2.5.0\nKernel version: 5.15.90.1\nWSLg version: 1.0.51\nMSRDC version: 1.2.3770\nDirect3D version: 1.608.2-61064218\nDXCore version: 10.0.25131.1002-220531-1700.rs-onecore-base2-hyp\nWindows version: 10.0.22621.2134', + stderr: '', + command: 'command', + }); + } + if (command === 'powershell.exe') { + resolve({ + stdout: 'True', + stderr: '', + command: 'command', + }); + } + }); + }); + const wslEnabled = await extension.isWSLEnabled(); + expect(wslEnabled).toBeTruthy(); +}); + +test('getJSONMachineList should only get machines from wsl if hyperv is not enabled', async () => { + vi.mocked(isWindows).mockReturnValue(true); + vi.mocked(isMac).mockReturnValue(false); + vi.spyOn(config, 'get').mockReturnValue(''); + vi.spyOn(extensionApi.process, 'exec').mockImplementation((command, args) => { + return new Promise(resolve => { + if (command !== 'wsl' && args?.[0] === '--version') { + resolve({ + stdout: 'podman version 5.1.1', + } as extensionApi.RunResult); + } + if (command === 'wsl') { + resolve({ + stdout: + 'WSL version: 2.2.5.0\nKernel version: 5.15.90.1\nWSLg version: 1.0.51\nMSRDC version: 1.2.3770\nDirect3D version: 1.608.2-61064218\nDXCore version: 10.0.25131.1002-220531-1700.rs-onecore-base2-hyp\nWindows version: 10.0.22621.2134', + stderr: '', + command: 'command', + }); + } + if (command === 'powershell.exe') { + resolve({ + stdout: 'True', + stderr: '', + command: 'command', + }); + } + }); + }); + const fakeJSON: extension.MachineJSON[] = [ + { + Name: 'podman-machine-default', + CPUs: 2, + Memory: '1048000000', + DiskSize: '250000000000', + Running: true, + Starting: false, + Default: true, + VMType: VMTYPE.LIBKRUN, + }, + { + Name: 'podman-machine-1', + CPUs: 2, + Memory: '1048000000', + DiskSize: '250000000000', + Running: false, + Starting: false, + Default: false, + VMType: VMTYPE.LIBKRUN, + }, + ]; + const execPodmanSpy = vi.spyOn(util, 'execPodman').mockResolvedValue({ + stdout: JSON.stringify(fakeJSON), + stderr: '', + command: '', + }); + await extension.getJSONMachineList(); + expect(execPodmanSpy).toBeCalledWith(['machine', 'list', '--format', 'json'], 'wsl'); +}); + +test('getJSONMachineList should only get machines from hyperv if wsl is not enabled', async () => { + vi.mocked(isWindows).mockReturnValue(true); + vi.mocked(isMac).mockReturnValue(false); + vi.spyOn(config, 'get').mockReturnValue(''); + vi.spyOn(extensionApi.process, 'exec').mockImplementation((command, args) => { + return new Promise(resolve => { + if (command !== 'wsl' && args?.[0] === '--version') { + resolve({ + stdout: 'podman version 5.1.1', + } as extensionApi.RunResult); + } + if (command === 'wsl') { + resolve({ + stdout: 'WSL version: invalid', + stderr: '', + command: 'command', + }); + } + if (command === 'powershell.exe') { + resolve({ + stdout: args?.[0] === '@(Get-Service vmms).Status' ? 'Running' : 'True', + stderr: '', + command: 'command', + }); + } + }); + }); + const fakeJSON: extension.MachineJSON[] = [ + { + Name: 'podman-machine-default', + CPUs: 2, + Memory: '1048000000', + DiskSize: '250000000000', + Running: true, + Starting: false, + Default: true, + VMType: VMTYPE.LIBKRUN, + }, + { + Name: 'podman-machine-1', + CPUs: 2, + Memory: '1048000000', + DiskSize: '250000000000', + Running: false, + Starting: false, + Default: false, + VMType: VMTYPE.LIBKRUN, + }, + ]; + const execPodmanSpy = vi.spyOn(util, 'execPodman').mockResolvedValue({ + stdout: JSON.stringify(fakeJSON), + stderr: '', + command: '', + }); + await extension.getJSONMachineList(); + expect(execPodmanSpy).toBeCalledWith(['machine', 'list', '--format', 'json'], 'hyperv'); +}); + +test('getJSONMachineList should get machines from hyperv and wsl if both are enabled', async () => { + vi.mocked(isWindows).mockReturnValue(true); + vi.mocked(isMac).mockReturnValue(false); + vi.spyOn(config, 'get').mockReturnValue(''); + vi.spyOn(extensionApi.process, 'exec').mockImplementation((command, args) => { + return new Promise(resolve => { + if (command !== 'wsl' && args?.[0] === '--version') { + resolve({ + stdout: 'podman version 5.1.1', + } as extensionApi.RunResult); + } + if (command === 'wsl') { + resolve({ + stdout: + 'WSL version: 2.2.5.0\nKernel version: 5.15.90.1\nWSLg version: 1.0.51\nMSRDC version: 1.2.3770\nDirect3D version: 1.608.2-61064218\nDXCore version: 10.0.25131.1002-220531-1700.rs-onecore-base2-hyp\nWindows version: 10.0.22621.2134', + stderr: '', + command: 'command', + }); + } + if (command === 'powershell.exe') { + resolve({ + stdout: args?.[0] === '@(Get-Service vmms).Status' ? 'Running' : 'True', + stderr: '', + command: 'command', + }); + } + }); + }); + const fakeJSON: extension.MachineJSON[] = [ + { + Name: 'podman-machine-default', + CPUs: 2, + Memory: '1048000000', + DiskSize: '250000000000', + Running: true, + Starting: false, + Default: true, + VMType: VMTYPE.LIBKRUN, + }, + { + Name: 'podman-machine-1', + CPUs: 2, + Memory: '1048000000', + DiskSize: '250000000000', + Running: false, + Starting: false, + Default: false, + VMType: VMTYPE.LIBKRUN, + }, + ]; + const execPodmanSpy = vi.spyOn(util, 'execPodman').mockResolvedValue({ + stdout: JSON.stringify(fakeJSON), + stderr: '', + command: '', + }); + await extension.getJSONMachineList(); + expect(execPodmanSpy).toHaveBeenNthCalledWith(1, ['machine', 'list', '--format', 'json'], 'wsl'); + expect(execPodmanSpy).toHaveBeenNthCalledWith(2, ['machine', 'list', '--format', 'json'], 'hyperv'); +}); diff --git a/extensions/podman/packages/extension/src/extension.ts b/extensions/podman/packages/extension/src/extension.ts index 9375ae34536cf..dcbf021b0f177 100644 --- a/extensions/podman/packages/extension/src/extension.ts +++ b/extensions/podman/packages/extension/src/extension.ts @@ -27,6 +27,7 @@ import * as extensionApi from '@podman-desktop/api'; import { compareVersions } from 'compare-versions'; import type { PodmanExtensionApi, PodmanRunOptions } from '../../api/src/podman-extension-api'; +import { SequenceCheck } from './base-check'; import { getSocketCompatibility } from './compatibility-mode'; import { getDetectionChecks } from './detection-checks'; import { KrunkitHelper } from './krunkit-helper'; @@ -37,7 +38,7 @@ import type { InstalledPodman } from './podman-cli'; import { getPodmanCli, getPodmanInstallation } from './podman-cli'; import { PodmanConfiguration } from './podman-configuration'; import { PodmanInfoHelper } from './podman-info-helper'; -import { PodmanInstall } from './podman-install'; +import { HyperVCheck, PodmanInstall, WSL2Check, WSLVersionCheck } from './podman-install'; import { PodmanRemoteConnections } from './podman-remote-connections'; import { QemuHelper } from './qemu-helper'; import { RegistrySetup } from './registry-setup'; @@ -1742,7 +1743,16 @@ export async function getJSONMachineList(): Promise { // if libkrun is supported we want to show both applehv and libkrun machines if (installedPodman && isLibkrunSupported(installedPodman.version)) { containerMachineProviders.push(...['applehv', 'libkrun']); - } else { + } + + if (await isWSLEnabled()) { + containerMachineProviders.push('wsl'); + } + if (await isHyperVEnabled()) { + containerMachineProviders.push('hyperv'); + } + + if (containerMachineProviders.length === 0) { // in all other cases we set undefined so that it executes normally by using the default container provider containerMachineProviders.push(undefined); } @@ -1807,6 +1817,24 @@ export function isLibkrunSupported(podmanVersion: string): boolean { return isMac() && compareVersions(podmanVersion, PODMAN_MINIMUM_VERSION_FOR_LIBKRUN_SUPPORT) >= 0; } +export async function isWSLEnabled(): Promise { + if (!isWindows()) { + return false; + } + const wslCheck = new SequenceCheck('WSL platform', [new WSLVersionCheck(), new WSL2Check()]); + const wslCheckResult = await wslCheck.execute(); + return wslCheckResult.successful; +} + +export async function isHyperVEnabled(): Promise { + if (!isWindows()) { + return false; + } + const hyperVCheck = new HyperVCheck(); + const hyperVCheckResult = await hyperVCheck.execute(); + return hyperVCheckResult.successful; +} + export function sendTelemetryRecords( eventName: string, telemetryRecords: Record, diff --git a/extensions/podman/packages/extension/src/podman-install.spec.ts b/extensions/podman/packages/extension/src/podman-install.spec.ts index 51ad55563f8ec..1771b5b54777c 100644 --- a/extensions/podman/packages/extension/src/podman-install.spec.ts +++ b/extensions/podman/packages/extension/src/podman-install.spec.ts @@ -639,10 +639,33 @@ describe('HyperV', () => { expect(result.docLinks?.[0].title).equal('Hyper-V Manual Installation Steps'); }); + test('expect HyperV preflight check return failure result if Podman Desktop is not run with elevated privileges', async () => { + let index = 0; + vi.spyOn(extensionApi.process, 'exec').mockImplementation(() => { + if (index++ < 1) { + return Promise.resolve({ + stdout: 'True', + stderr: '', + command: 'command', + }); + } else { + throw new Error(); + } + }); + + const hyperVCheck = new HyperVCheck(); + const result = await hyperVCheck.execute(); + expect(result.successful).toBeFalsy(); + expect(result.description).equal( + 'You must run Podman Desktop with administrative rights to run Hyper-V Podman machines.', + ); + expect(result.docLinks).toBeUndefined(); + }); + test('expect HyperV preflight check return failure result if HyperV not installed', async () => { let index = 0; vi.spyOn(extensionApi.process, 'exec').mockImplementation(() => { - if (index++ === 0) { + if (index++ <= 1) { return Promise.resolve({ stdout: 'True', stderr: '', @@ -666,7 +689,7 @@ describe('HyperV', () => { test('expect HyperV preflight check return failure result if HyperV not running', async () => { let index = 0; vi.spyOn(extensionApi.process, 'exec').mockImplementation(() => { - if (index++ < 2) { + if (index++ <= 2) { return Promise.resolve({ stdout: 'True', stderr: '', @@ -690,9 +713,9 @@ describe('HyperV', () => { test('expect HyperV preflight check return OK', async () => { let index = 0; vi.spyOn(extensionApi.process, 'exec').mockImplementation(() => { - if (index++ < 3) { + if (index++ < 4) { return Promise.resolve({ - stdout: index === 3 ? 'Running' : 'True', + stdout: index === 4 ? 'Running' : 'True', stderr: '', command: 'command', }); diff --git a/extensions/podman/packages/extension/src/podman-install.ts b/extensions/podman/packages/extension/src/podman-install.ts index 7b130473c1925..869e3063c8d86 100755 --- a/extensions/podman/packages/extension/src/podman-install.ts +++ b/extensions/podman/packages/extension/src/podman-install.ts @@ -668,19 +668,21 @@ export class WSL2Check extends WindowsCheck { title = 'WSL2 Installed'; installWSLCommandId = 'podman.onboarding.installWSL'; - constructor(private extensionContext: extensionApi.ExtensionContext) { + constructor(private extensionContext?: extensionApi.ExtensionContext) { super(); } async init(): Promise { - const wslCommand = extensionApi.commands.registerCommand(this.installWSLCommandId, async () => { - const installSucceeded = await this.installWSL(); - if (installSucceeded) { - // if action succeeded, do a re-check of all podman requirements so user can be moved forward if all missing pieces have been installed - await extensionApi.commands.executeCommand('podman.onboarding.checkRequirementsCommand'); - } - }); - this.extensionContext.subscriptions.push(wslCommand); + if (this.extensionContext) { + const wslCommand = extensionApi.commands.registerCommand(this.installWSLCommandId, async () => { + const installSucceeded = await this.installWSL(); + if (installSucceeded) { + // if action succeeded, do a re-check of all podman requirements so user can be moved forward if all missing pieces have been installed + await extensionApi.commands.executeCommand('podman.onboarding.checkRequirementsCommand'); + } + }); + this.extensionContext.subscriptions.push(wslCommand); + } } async execute(): Promise { @@ -831,6 +833,11 @@ export class HyperVCheck extends WindowsCheck { }, }); } + if (!(await this.isPodmanDesktopElevated())) { + return this.createFailureResult({ + description: 'You must run Podman Desktop with administrative rights to run Hyper-V Podman machines.', + }); + } if (!(await this.isHyperVinstalled())) { return this.createFailureResult({ description: 'Hyper-V is not installed on your system.', @@ -854,6 +861,17 @@ export class HyperVCheck extends WindowsCheck { return this.createSuccessfulResult(); } + private async isPodmanDesktopElevated(): Promise { + try { + const { stdout: res } = await extensionApi.process.exec('powershell.exe', [ + '(New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)', + ]); + return res.trim() === 'True'; + } catch (err: unknown) { + return false; + } + } + private async isHyperVinstalled(): Promise { try { await extensionApi.process.exec('powershell.exe', ['Get-Service vmms']);