Skip to content

Commit 425afb1

Browse files
authored
send telemetry related to krunkit (podman-desktop#8680)
* feat: send telemetry related to krunkit Signed-off-by: Philippe Martin <[email protected]> * test: add unit tests for sendTelemetryRecords Signed-off-by: Philippe Martin <[email protected]> * feat: send telemetry when stop machine Signed-off-by: Philippe Martin <[email protected]> * fix: send provider code instead of label Signed-off-by: Philippe Martin <[email protected]> * chore: keep qemu-helper for future use on Linux Signed-off-by: Philippe Martin <[email protected]> --------- Signed-off-by: Philippe Martin <[email protected]>
1 parent d255d95 commit 425afb1

File tree

4 files changed

+289
-27
lines changed

4 files changed

+289
-27
lines changed

extensions/podman/src/extension.spec.ts

+128-8
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ const telemetryLogger: extensionApi.TelemetryLogger = {
104104
logError: vi.fn(),
105105
} as unknown as extensionApi.TelemetryLogger;
106106

107+
const mocks = vi.hoisted(() => ({
108+
getPodmanLocationMacMock: vi.fn(),
109+
getKrunkitVersionMock: vi.fn(),
110+
}));
111+
107112
// mock ps-list
108113
vi.mock('ps-list', async () => {
109114
return {
@@ -218,13 +223,11 @@ vi.mock('node:os', async () => {
218223
};
219224
});
220225

221-
vi.mock('./qemu-helper', async () => {
226+
vi.mock('./krunkit-helper', async () => {
222227
return {
223-
QemuHelper: vi.fn().mockImplementation(() => {
228+
KrunkitHelper: vi.fn().mockImplementation(() => {
224229
return {
225-
getQemuVersion: vi.fn().mockImplementation(() => {
226-
return Promise.resolve('1.2.3');
227-
}),
230+
getKrunkitVersion: mocks.getKrunkitVersionMock,
228231
};
229232
}),
230233
};
@@ -233,9 +236,7 @@ vi.mock('./podman-binary-location-helper', async () => {
233236
return {
234237
PodmanBinaryLocationHelper: vi.fn().mockImplementation(() => {
235238
return {
236-
getPodmanLocationMac: vi.fn().mockImplementation(() => {
237-
return Promise.resolve({ source: 'unknown' });
238-
}),
239+
getPodmanLocationMac: mocks.getPodmanLocationMacMock,
239240
};
240241
}),
241242
};
@@ -2096,3 +2097,122 @@ test('isLibkrunSupported should return false with previous 5.1.2 version', async
20962097
const enabled = extension.isLibkrunSupported('5.1.2');
20972098
expect(enabled).toBeFalsy();
20982099
});
2100+
2101+
test('sendTelemetryRecords with krunkit found', async () => {
2102+
vi.spyOn(podmanCli, 'getPodmanInstallation').mockResolvedValue({
2103+
version: '5.1.2',
2104+
});
2105+
mocks.getPodmanLocationMacMock.mockResolvedValue({ foundPath: '/opt/podman/bin/podman', source: 'installer' });
2106+
mocks.getKrunkitVersionMock.mockResolvedValue('1.2.3');
2107+
2108+
extension.sendTelemetryRecords(
2109+
'evt',
2110+
{
2111+
provider: 'libkrun',
2112+
} as Record<string, unknown>,
2113+
false,
2114+
);
2115+
await new Promise(resolve => setTimeout(resolve, 100));
2116+
expect(telemetryLogger.logUsage).toHaveBeenCalledWith(
2117+
'evt',
2118+
expect.objectContaining({
2119+
krunkitPath: '/opt/podman/bin',
2120+
krunkitVersion: '1.2.3',
2121+
podmanCliFoundPath: '/opt/podman/bin/podman',
2122+
podmanCliSource: 'installer',
2123+
podmanCliVersion: '5.1.2',
2124+
provider: 'libkrun',
2125+
}),
2126+
);
2127+
});
2128+
2129+
test('sendTelemetryRecords with krunkit not found', async () => {
2130+
vi.spyOn(podmanCli, 'getPodmanInstallation').mockResolvedValue({
2131+
version: '5.1.2',
2132+
});
2133+
mocks.getPodmanLocationMacMock.mockResolvedValue({ foundPath: '/opt/podman/bin/podman', source: 'installer' });
2134+
mocks.getKrunkitVersionMock.mockRejectedValue('command not found');
2135+
2136+
extension.sendTelemetryRecords(
2137+
'evt',
2138+
{
2139+
provider: 'libkrun',
2140+
} as Record<string, unknown>,
2141+
false,
2142+
);
2143+
await new Promise(resolve => setTimeout(resolve, 100));
2144+
expect(telemetryLogger.logUsage).toHaveBeenCalledWith(
2145+
'evt',
2146+
expect.objectContaining({
2147+
errorKrunkitVersion: 'command not found',
2148+
podmanCliFoundPath: '/opt/podman/bin/podman',
2149+
podmanCliSource: 'installer',
2150+
podmanCliVersion: '5.1.2',
2151+
provider: 'libkrun',
2152+
}),
2153+
);
2154+
});
2155+
2156+
test('if a machine stopped is successfully reporting telemetry', async () => {
2157+
const spyExecPromise = vi
2158+
.spyOn(extensionApi.process, 'exec')
2159+
.mockImplementation(() => Promise.resolve({} as extensionApi.RunResult));
2160+
vi.spyOn(podmanCli, 'getPodmanInstallation').mockResolvedValue({
2161+
version: '5.1.2',
2162+
});
2163+
mocks.getPodmanLocationMacMock.mockResolvedValue({ foundPath: '/opt/podman/bin/podman', source: 'installer' });
2164+
mocks.getKrunkitVersionMock.mockResolvedValue('1.2.3');
2165+
await extension.stopMachine(provider, machineInfo);
2166+
2167+
// wait a call on telemetryLogger.logUsage
2168+
while ((telemetryLogger.logUsage as Mock).mock.calls.length === 0) {
2169+
await new Promise(resolve => setTimeout(resolve, 100));
2170+
}
2171+
2172+
expect(telemetryLogger.logUsage).toBeCalledWith(
2173+
'podman.machine.stop',
2174+
expect.objectContaining({
2175+
krunkitPath: '/opt/podman/bin',
2176+
krunkitVersion: '1.2.3',
2177+
podmanCliFoundPath: '/opt/podman/bin/podman',
2178+
podmanCliSource: 'installer',
2179+
podmanCliVersion: '5.1.2',
2180+
provider: 'libkrun',
2181+
}),
2182+
);
2183+
expect(spyExecPromise).toBeCalledWith(podmanCli.getPodmanCli(), ['machine', 'stop', 'name'], expect.anything());
2184+
});
2185+
2186+
test('if a machine stopped is successfully reporting an error in telemetry', async () => {
2187+
const customError = new Error('Error while starting podman');
2188+
2189+
const spyExecPromise = vi.spyOn(extensionApi.process, 'exec').mockImplementation(() => {
2190+
throw customError;
2191+
});
2192+
vi.spyOn(podmanCli, 'getPodmanInstallation').mockResolvedValue({
2193+
version: '5.1.2',
2194+
});
2195+
mocks.getPodmanLocationMacMock.mockResolvedValue({ foundPath: '/opt/podman/bin/podman', source: 'installer' });
2196+
mocks.getKrunkitVersionMock.mockResolvedValue('1.2.3');
2197+
await expect(extension.stopMachine(provider, machineInfo)).rejects.toThrow(customError.message);
2198+
2199+
// wait a call on telemetryLogger.logUsage
2200+
while ((telemetryLogger.logUsage as Mock).mock.calls.length === 0) {
2201+
await new Promise(resolve => setTimeout(resolve, 100));
2202+
}
2203+
2204+
expect(telemetryLogger.logUsage).toBeCalledWith(
2205+
'podman.machine.stop',
2206+
expect.objectContaining({
2207+
krunkitPath: '/opt/podman/bin',
2208+
krunkitVersion: '1.2.3',
2209+
podmanCliFoundPath: '/opt/podman/bin/podman',
2210+
podmanCliSource: 'installer',
2211+
podmanCliVersion: '5.1.2',
2212+
error: customError,
2213+
provider: 'libkrun',
2214+
}),
2215+
);
2216+
2217+
expect(spyExecPromise).toBeCalledWith(podmanCli.getPodmanCli(), ['machine', 'stop', 'name'], expect.anything());
2218+
});

extensions/podman/src/extension.ts

+45-19
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { compareVersions } from 'compare-versions';
2828

2929
import { getSocketCompatibility } from './compatibility-mode';
3030
import { getDetectionChecks } from './detection-checks';
31+
import { KrunkitHelper } from './krunkit-helper';
3132
import { PodmanBinaryLocationHelper } from './podman-binary-location-helper';
3233
import { PodmanCleanupMacOS } from './podman-cleanup-macos';
3334
import { PodmanCleanupWindows } from './podman-cleanup-windows';
@@ -37,7 +38,6 @@ import { PodmanConfiguration } from './podman-configuration';
3738
import { PodmanInfoHelper } from './podman-info-helper';
3839
import { PodmanInstall } from './podman-install';
3940
import { PodmanRemoteConnections } from './podman-remote-connections';
40-
import { QemuHelper } from './qemu-helper';
4141
import { RegistrySetup } from './registry-setup';
4242
import {
4343
appConfigDir,
@@ -89,7 +89,7 @@ const configurationCompatibilityMode = 'setting.dockerCompatibility';
8989
let telemetryLogger: extensionApi.TelemetryLogger | undefined;
9090

9191
const wslHelper = new WslHelper();
92-
const qemuHelper = new QemuHelper();
92+
const krunkitHelper = new KrunkitHelper();
9393
const podmanBinaryHelper = new PodmanBinaryLocationHelper();
9494
const podmanInfoHelper = new PodmanInfoHelper();
9595

@@ -746,10 +746,7 @@ export async function registerProviderFor(
746746
await startMachine(provider, podmanConfiguration, machineInfo, context, logger, undefined, false);
747747
},
748748
stop: async (context, logger): Promise<void> => {
749-
await execPodman(['machine', 'stop', machineInfo.name], machineInfo.vmType, {
750-
logger: new LoggerDelegator(context, logger),
751-
});
752-
provider.updateStatus('stopped');
749+
await stopMachine(provider, machineInfo, context, logger);
753750
},
754751
delete: async (logger): Promise<void> => {
755752
await execPodman(['machine', 'rm', '-f', machineInfo.name], machineInfo.vmType, {
@@ -871,6 +868,7 @@ export async function startMachine(
871868
autoStart?: boolean,
872869
): Promise<void> {
873870
const telemetryRecords: Record<string, unknown> = {};
871+
telemetryRecords.provider = machineInfo.vmType;
874872
const startTime = performance.now();
875873

876874
await checkRosettaMacArm(podmanConfiguration);
@@ -898,6 +896,31 @@ export async function startMachine(
898896
}
899897
}
900898

899+
export async function stopMachine(
900+
provider: extensionApi.Provider,
901+
machineInfo: MachineInfo,
902+
context?: extensionApi.LifecycleContext,
903+
logger?: extensionApi.Logger,
904+
): Promise<void> {
905+
const startTime = performance.now();
906+
const telemetryRecords: Record<string, unknown> = {};
907+
telemetryRecords.provider = machineInfo.vmType;
908+
try {
909+
await execPodman(['machine', 'stop', machineInfo.name], machineInfo.vmType, {
910+
logger: new LoggerDelegator(context, logger),
911+
});
912+
provider.updateStatus('stopped');
913+
} catch (err: unknown) {
914+
telemetryRecords.error = err;
915+
throw err;
916+
} finally {
917+
// send telemetry event
918+
const endTime = performance.now();
919+
telemetryRecords.duration = endTime - startTime;
920+
sendTelemetryRecords('podman.machine.stop', telemetryRecords, false);
921+
}
922+
}
923+
901924
async function doHandleError(
902925
provider: extensionApi.Provider,
903926
machineInfo: MachineInfo,
@@ -1764,7 +1787,7 @@ export function isLibkrunSupported(podmanVersion: string): boolean {
17641787
return isMac() && compareVersions(podmanVersion, PODMAN_MINIMUM_VERSION_FOR_LIBKRUN_SUPPORT) >= 0;
17651788
}
17661789

1767-
function sendTelemetryRecords(
1790+
export function sendTelemetryRecords(
17681791
eventName: string,
17691792
telemetryRecords: Record<string, unknown>,
17701793
includeMachineStats: boolean,
@@ -1784,16 +1807,16 @@ function sendTelemetryRecords(
17841807
telemetryRecords.hostCpuModel = hostCpus[0].model;
17851808

17861809
// on macOS, try to see if podman is coming from brew or from the installer
1787-
// and display version of qemu
1810+
// and display version of krunkit
17881811
if (extensionApi.env.isMac) {
1789-
let qemuPath: string | undefined;
1812+
let krunkitPath: string | undefined;
17901813

17911814
try {
17921815
const podmanBinaryResult = await podmanBinaryHelper.getPodmanLocationMac();
17931816

17941817
telemetryRecords.podmanCliSource = podmanBinaryResult.source;
17951818
if (podmanBinaryResult.source === 'installer') {
1796-
qemuPath = '/opt/podman/qemu/bin';
1819+
krunkitPath = '/opt/podman/bin';
17971820
}
17981821
telemetryRecords.podmanCliFoundPath = podmanBinaryResult.foundPath;
17991822
if (podmanBinaryResult.error) {
@@ -1804,16 +1827,18 @@ function sendTelemetryRecords(
18041827
console.trace('unable to check from which path podman is coming', error);
18051828
}
18061829

1807-
// add qemu version
1808-
try {
1809-
const qemuVersion = await qemuHelper.getQemuVersion(qemuPath);
1810-
if (qemuPath) {
1811-
telemetryRecords.qemuPath = qemuPath;
1830+
if (telemetryRecords.provider === 'libkrun') {
1831+
// add krunkit version
1832+
try {
1833+
const krunkitVersion = await krunkitHelper.getKrunkitVersion(krunkitPath);
1834+
if (krunkitPath) {
1835+
telemetryRecords.krunkitPath = krunkitPath;
1836+
}
1837+
telemetryRecords.krunkitVersion = krunkitVersion;
1838+
} catch (error) {
1839+
console.trace('unable to check krunkit version', error);
1840+
telemetryRecords.errorKrunkitVersion = error;
18121841
}
1813-
telemetryRecords.qemuVersion = qemuVersion;
1814-
} catch (error) {
1815-
console.trace('unable to check qemu version', error);
1816-
telemetryRecords.errorQemuVersion = error;
18171842
}
18181843
} else if (extensionApi.env.isWindows) {
18191844
// try to get wsl version
@@ -1856,6 +1881,7 @@ export async function createMachine(
18561881
let provider: string | undefined;
18571882
if (params['podman.factory.machine.provider']) {
18581883
provider = getProviderByLabel(params['podman.factory.machine.provider']);
1884+
telemetryRecords.provider = provider;
18591885
}
18601886

18611887
// cpus
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**********************************************************************
2+
* Copyright (C) 2024 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
import * as extensionApi from '@podman-desktop/api';
20+
import type { Mock } from 'vitest';
21+
import { beforeEach, expect, test, vi } from 'vitest';
22+
23+
import { KrunkitHelper } from './krunkit-helper';
24+
25+
let krunkitHelper: KrunkitHelper;
26+
27+
// mock the API
28+
vi.mock('@podman-desktop/api', async () => {
29+
return {
30+
process: {
31+
exec: vi.fn(),
32+
},
33+
};
34+
});
35+
36+
beforeEach(() => {
37+
krunkitHelper = new KrunkitHelper();
38+
vi.resetAllMocks();
39+
});
40+
41+
test('should grab correct version', async () => {
42+
const output = `krunkit 0.1.2
43+
`;
44+
45+
(extensionApi.process.exec as Mock).mockReturnValue({
46+
stdout: output,
47+
} as extensionApi.RunResult);
48+
49+
// use a specific arch for the test
50+
const version = await krunkitHelper.getKrunkitVersion();
51+
52+
expect(version).toBe('0.1.2');
53+
54+
// expect called with qemu-system-aarch64 (as it's arm64)
55+
expect(extensionApi.process.exec).toHaveBeenCalledWith('krunkit', ['--version'], undefined);
56+
});
57+
58+
test('should grab correct version using a given path', async () => {
59+
const output = `krunkit 0.1.2
60+
`;
61+
62+
(extensionApi.process.exec as Mock).mockReturnValue({
63+
stdout: output,
64+
} as extensionApi.RunResult);
65+
66+
const fakePath = '/my-dummy-path';
67+
68+
const version = await krunkitHelper.getKrunkitVersion(fakePath);
69+
70+
expect(version).toBe('0.1.2');
71+
72+
expect(extensionApi.process.exec).toHaveBeenCalledWith('krunkit', ['--version'], {
73+
env: {
74+
PATH: fakePath,
75+
},
76+
});
77+
});

0 commit comments

Comments
 (0)