Skip to content

Commit

Permalink
feat: detect podman v4 qemu machines after update and delete them (po…
Browse files Browse the repository at this point in the history
…dman-desktop#6565)

* feat: detect podman v4 qemu machines after update and delete them

It is part of the onboarding workflow

fixes podman-desktop#6559
Signed-off-by: Florent Benoit <[email protected]>
  • Loading branch information
benoitf authored Mar 29, 2024
1 parent ac8635d commit b66757e
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 3 deletions.
47 changes: 45 additions & 2 deletions extensions/podman/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@
{
"command": "podman.onboarding.installPodman",
"title": "Podman: Install podman"
},
{
"command": "podman.onboarding.checkUnsupportedPodmanMachine",
"title": "Podman: Check if an old unsupported podman machine exists"
},
{
"command": "podman.onboarding.removeUnsupportedMachines",
"title": "Podman: Remove unsupported podman machines"
}
],
"configuration": {
Expand Down Expand Up @@ -260,7 +268,7 @@
{
"id": "installationSuccessView",
"title": "Podman settings",
"when": "!onboardingContext:podmanIsNotInstalled",
"when": "!onboardingContext:podmanIsNotInstalled && !podman.needPodmanMachineCleanup",
"content": [
[
{
Expand All @@ -269,6 +277,41 @@
]
]
},
{
"id": "checkUnsupportedPodmanMachineCommand",
"title": "Checking if an old unsupported Podman machine exists",
"when": "!isLinux",
"command": "podman.onboarding.checkUnsupportedPodmanMachine",
"completionEvents": [
"onCommand:podman.onboarding.checkUnsupportedPodmanMachine"
]
},
{
"id": "welcomeFixUnsupportedPodmanMachineView",
"title": "Unsupported Podman machine detected",
"description": "Podman Desktop will remove the unsupported machines",
"completionEvents": [
"onboardingContext:unsupportedMachineRemoved == ok"
],
"content": [
[
{
"value": "Podman Machines created with podman v4 are no longer supported by podman v5"
}
],
[
{
"value": ":button[Remove unsupported Podman machine(s)]{command=podman.onboarding.removeUnsupportedMachines}"
}
],
[
{
"value": "Interrupt the process by clicking on `skip` or `skip the entire setup`"
}
]
],
"when": "onboardingContext:unsupportedPodmanMachine && !isLinux"
},
{
"id": "checkPodmanMachineExistsCommand",
"title": "Checking if a Podman machine exists",
Expand Down Expand Up @@ -309,7 +352,7 @@
]
}
],
"enablement": "(isLinux && onboardingContext:podmanIsNotInstalled) || (!isLinux && !onboardingContext:podmanMachineExists)"
"enablement": "(isLinux && onboardingContext:podmanIsNotInstalled) || (!isLinux && !onboardingContext:podmanMachineExists) || podman.needPodmanMachineCleanup"
}
},
"scripts": {
Expand Down
139 changes: 139 additions & 0 deletions extensions/podman/src/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
checkDisguisedPodmanSocket,
initCheckAndRegisterUpdate,
registerOnboardingMachineExistsCommand,
registerOnboardingUnsupportedPodmanMachineCommand,
} from './extension';
import * as extension from './extension';
import type { InstalledPodman } from './podman-cli';
Expand Down Expand Up @@ -173,6 +174,7 @@ vi.mock('@podman-desktop/api', async () => {
window: {
showErrorMessage: vi.fn(),
showInformationMessage: vi.fn(),
showWarningMessage: vi.fn(),
showNotification: vi.fn(),
},
context: {
Expand Down Expand Up @@ -903,6 +905,8 @@ test('ensure started machine reports default configuration', async () => {
resolve({
stdout: JSON.stringify([{ Name: fakeMachineJSON[0].Name, Default: true }]),
} as extensionApi.RunResult);
} else if (args[0] === '--version') {
resolve({ stdout: 'podman version 4.9.0' } as extensionApi.RunResult);
}
}),
);
Expand All @@ -929,6 +933,8 @@ test('ensure started machine reports configuration', async () => {
resolve({
stdout: JSON.stringify([{ Name: fakeMachineJSON[0].Name, Default: true }]),
} as extensionApi.RunResult);
} else if (args[0] === '--version') {
resolve({ stdout: 'podman version 4.9.0' } as extensionApi.RunResult);
}
}),
);
Expand Down Expand Up @@ -964,6 +970,8 @@ test('ensure stopped machine reports configuration', async () => {
resolve({
stdout: JSON.stringify([{ Name: fakeMachineJSON[1].Name, Default: true }]),
} as extensionApi.RunResult);
} else if (args[0] === '--version') {
resolve({ stdout: 'podman version 4.9.0' } as extensionApi.RunResult);
}
}),
);
Expand Down Expand Up @@ -1367,3 +1375,134 @@ describe('registerOnboardingMachineExistsCommand', () => {
expect(extensionApi.context.setValue).toBeCalledWith('podmanMachineExists', false, 'onboarding');
});
});

describe('registerOnboardingUnsupportedPodmanMachineCommand', () => {
test('check with v5 and previous qemu folders', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);

vi.mocked(extensionApi.commands.registerCommand).mockReturnValue({ dispose: vi.fn() });

vi.mocked(extensionApi.process.exec).mockResolvedValue({
stdout: 'podman version 5.0.0',
} as unknown as extensionApi.RunResult);

// perform the call
const disposable = registerOnboardingUnsupportedPodmanMachineCommand();

// checks
expect(disposable).toBeDefined();

// check command is called
expect(extensionApi.commands.registerCommand).toBeCalledWith(
'podman.onboarding.checkUnsupportedPodmanMachine',
expect.any(Function),
);

const func = vi.mocked(extensionApi.commands.registerCommand).mock.calls[0][1];
// call the function
await func();

// check called with true as there are qemu folders
expect(extensionApi.context.setValue).toBeCalledWith('unsupportedPodmanMachine', true, 'onboarding');
});

test('check with v5 and no previous qemu folders', async () => {
// no qemu folders
vi.mocked(fs.existsSync).mockReturnValue(false);

vi.mocked(extensionApi.commands.registerCommand).mockReturnValue({ dispose: vi.fn() });

vi.mocked(extensionApi.process.exec).mockResolvedValue({
stdout: 'podman version 5.0.0',
} as unknown as extensionApi.RunResult);

// perform the call
const disposable = registerOnboardingUnsupportedPodmanMachineCommand();

// checks
expect(disposable).toBeDefined();

// check command is called
expect(extensionApi.commands.registerCommand).toBeCalledWith(
'podman.onboarding.checkUnsupportedPodmanMachine',
expect.any(Function),
);

const func = vi.mocked(extensionApi.commands.registerCommand).mock.calls[0][1];
// call the function
await func();

// check it is false as there are no qemu folders
expect(extensionApi.context.setValue).toBeCalledWith('unsupportedPodmanMachine', false, 'onboarding');
});

test('check with v4 and qemu folders', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);

vi.mocked(extensionApi.commands.registerCommand).mockReturnValue({ dispose: vi.fn() });

vi.mocked(extensionApi.process.exec).mockResolvedValue({
stdout: 'podman version 4.9.3',
} as unknown as extensionApi.RunResult);

// perform the call
const disposable = registerOnboardingUnsupportedPodmanMachineCommand();

// checks
expect(disposable).toBeDefined();

// check command is called
expect(extensionApi.commands.registerCommand).toBeCalledWith(
'podman.onboarding.checkUnsupportedPodmanMachine',
expect.any(Function),
);

const func = vi.mocked(extensionApi.commands.registerCommand).mock.calls[0][1];
// call the function
await func();

// check called with false as there are qemu folders but we're with podman v4
expect(extensionApi.context.setValue).toBeCalledWith('unsupportedPodmanMachine', false, 'onboarding');
});
});

describe('registerOnboardingRemoveUnsupportedMachinesCommand', () => {
test('check with previous qemu folders', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);

// mock confirmation window message to true
vi.mocked(extensionApi.window.showWarningMessage).mockResolvedValue('Yes');

vi.mocked(extensionApi.commands.registerCommand).mockReturnValue({ dispose: vi.fn() });

vi.mocked(extensionApi.process.exec).mockResolvedValue({
stdout: 'podman version 5.0.0',
} as unknown as extensionApi.RunResult);

// perform the call
const disposable = extension.registerOnboardingRemoveUnsupportedMachinesCommand();

// checks
expect(disposable).toBeDefined();

// check command is called
expect(extensionApi.commands.registerCommand).toBeCalledWith(
'podman.onboarding.removeUnsupportedMachines',
expect.any(Function),
);

const func = vi.mocked(extensionApi.commands.registerCommand).mock.calls[0][1];
// call the function
await func();

// expect rm to be called
expect(fs.promises.rm).toBeCalledWith(expect.stringContaining('qemu'), {
recursive: true,
maxRetries: 3,
retryDelay: 1000,
});

// check called with true as there are qemu folders
expect(extensionApi.context.setValue).toBeCalledWith('unsupportedMachineRemoved', 'ok', 'onboarding');
});
});
87 changes: 86 additions & 1 deletion extensions/podman/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { PodmanInfoHelper } from './podman-info-helper';
import { PODMAN5_EXPERIMENTAL_MODE_CONFIG_FULLKEY, PodmanInstall } from './podman-install';
import { QemuHelper } from './qemu-helper';
import { RegistrySetup } from './registry-setup';
import { appHomeDir, getAssetsFolder, isLinux, isMac, isWindows, LoggerDelegator } from './util';
import { appConfigDir, appHomeDir, getAssetsFolder, isLinux, isMac, isWindows, LoggerDelegator } from './util';
import { getDisguisedPodmanInformation, getSocketPath, isDisguisedPodman } from './warnings';
import { WslHelper } from './wsl-helper';

Expand Down Expand Up @@ -145,6 +145,13 @@ export async function updateMachines(provider: extensionApi.Provider): Promise<v
const machines = JSON.parse(machineListOutput.stdout) as MachineJSON[];
extensionApi.context.setValue('podmanMachineExists', machines.length > 0, 'onboarding');

const installedPodman = await getPodmanInstallation();
let shouldCleanMachine = false;
if (installedPodman) {
shouldCleanMachine = shouldNotifyQemuMachinesWithV5(installedPodman);
}
extensionApi.context.setValue(CLEANUP_REQUIRED_MACHINE_KEY, shouldCleanMachine);

// Only show the notification on macOS and Windows
// as Podman is already installed on Linux and machine is OPTIONAL.
if (shouldNotifySetup && machines.length === 0 && !isLinux()) {
Expand Down Expand Up @@ -555,6 +562,20 @@ async function monitorMachines(provider: extensionApi.Provider): Promise<void> {
}
}

function shouldNotifyQemuMachinesWithV5(installedPodman: InstalledPodman): boolean {
// if on macOS we have some files from qemu it needs to be removed/cleaned
// check if the qemu files are present in ~/.config/containers/podman/machine/qemu or ~/.local/share/containers/podman/machine/qemu
// get current podman version
if (extensionApi.env.isMac && installedPodman.version.startsWith('5.')) {
const qemuSharePath = path.resolve(os.homedir(), appHomeDir(), 'machine', 'qemu');
const qemuConfigPath = path.resolve(os.homedir(), appConfigDir(), 'machine', 'qemu');
if (fs.existsSync(qemuSharePath) || fs.existsSync(qemuConfigPath)) {
return true;
}
}
return false;
}

async function monitorProvider(provider: extensionApi.Provider): Promise<void> {
// call us again
if (!stopLoop) {
Expand All @@ -580,6 +601,7 @@ async function monitorProvider(provider: extensionApi.Provider): Promise<void> {
if (provider.status === 'not-installed') {
provider.updateStatus('installed');
}

extensionApi.context.setValue('podmanIsNotInstalled', false, 'onboarding');
// if podman has been installed, we reset the notification flag so if podman is uninstalled in future we can show the notification again
if (isLinux()) {
Expand Down Expand Up @@ -819,6 +841,7 @@ export async function registerUpdatesIfAny(
export const ROOTFUL_MACHINE_INIT_SUPPORTED_KEY = 'podman.isRootfulMachineInitSupported';
export const USER_MODE_NETWORKING_SUPPORTED_KEY = 'podman.isUserModeNetworkingSupported';
export const START_NOW_MACHINE_INIT_SUPPORTED_KEY = 'podman.isStartNowAtMachineInitSupported';
export const CLEANUP_REQUIRED_MACHINE_KEY = 'podman.needPodmanMachineCleanup';

export function initTelemetryLogger(): void {
telemetryLogger = extensionApi.env.createTelemetryLogger();
Expand Down Expand Up @@ -889,6 +912,64 @@ export function registerOnboardingMachineExistsCommand(): extensionApi.Disposabl
});
}

export function registerOnboardingUnsupportedPodmanMachineCommand(): extensionApi.Disposable {
return extensionApi.commands.registerCommand('podman.onboarding.checkUnsupportedPodmanMachine', async () => {
let isUnsupported = false;
const installedPodman = await getPodmanInstallation();
if (installedPodman) {
isUnsupported = shouldNotifyQemuMachinesWithV5(installedPodman);
}

extensionApi.context.setValue('unsupportedPodmanMachine', isUnsupported, 'onboarding');
});
}

export function registerOnboardingRemoveUnsupportedMachinesCommand(): extensionApi.Disposable {
return extensionApi.commands.registerCommand('podman.onboarding.removeUnsupportedMachines', async () => {
// only on macOS
// do not check if version is v5 as it is being checked by the command that triggers this one
if (extensionApi.env.isMac) {
// remove the qemu machines folder
const qemuSharePath = path.resolve(os.homedir(), appHomeDir(), 'machine', 'qemu');
const qemuConfigPath = path.resolve(os.homedir(), appConfigDir(), 'machine', 'qemu');

// remove folders if exists
const foldersToRemove = [];
if (fs.existsSync(qemuSharePath)) {
foldersToRemove.push(qemuSharePath);
}
if (fs.existsSync(qemuConfigPath)) {
foldersToRemove.push(qemuConfigPath);
}

// prompt the user to confirm
const result = await extensionApi.window.showWarningMessage(
'Removing old unsupported Podman machines will delete all of their data. Confirm approval?',
'Yes',
'No',
);
if (result === 'No') {
return;
}

const errors: string[] = [];
for (const folder of foldersToRemove) {
try {
await fs.promises.rm(folder, { recursive: true, retryDelay: 1000, maxRetries: 3 });
} catch (error) {
console.error('Error removing folder', folder, error);
errors.push(`Unable to remove the folder ${folder}: ${String(error)}`);
}
}
if (errors.length > 0) {
await extensionApi.window.showErrorMessage(`Error removing unsupported Podman machines. ${errors.join('\n')}`);
}
}

extensionApi.context.setValue('unsupportedMachineRemoved', 'ok', 'onboarding');
});
}

export async function activate(extensionContext: extensionApi.ExtensionContext): Promise<void> {
initExtensionContext(extensionContext);

Expand Down Expand Up @@ -1193,6 +1274,8 @@ export async function activate(extensionContext: extensionApi.ExtensionContext):
});
},
);
const onboardingUnsupportedPodmanMachineCommand = registerOnboardingUnsupportedPodmanMachineCommand();
const onboardingRemoveUnsupportedMachinesCommand = registerOnboardingRemoveUnsupportedMachinesCommand();

const onboardingCheckPodmanMachineExistsCommand = registerOnboardingMachineExistsCommand();

Expand Down Expand Up @@ -1281,6 +1364,8 @@ export async function activate(extensionContext: extensionApi.ExtensionContext):
onboardingCheckPodmanMachineExistsCommand,
onboardingCheckReqsCommand,
onboardingInstallPodmanCommand,
onboardingUnsupportedPodmanMachineCommand,
onboardingRemoveUnsupportedMachinesCommand,
);

// register the registries
Expand Down
Loading

0 comments on commit b66757e

Please sign in to comment.