mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-05-24 10:18:53 +00:00
send telemetry related to krunkit (#8680)
* feat: send telemetry related to krunkit Signed-off-by: Philippe Martin <phmartin@redhat.com> * test: add unit tests for sendTelemetryRecords Signed-off-by: Philippe Martin <phmartin@redhat.com> * feat: send telemetry when stop machine Signed-off-by: Philippe Martin <phmartin@redhat.com> * fix: send provider code instead of label Signed-off-by: Philippe Martin <phmartin@redhat.com> * chore: keep qemu-helper for future use on Linux Signed-off-by: Philippe Martin <phmartin@redhat.com> --------- Signed-off-by: Philippe Martin <phmartin@redhat.com>
This commit is contained in:
parent
d255d95bb3
commit
425afb11da
4 changed files with 289 additions and 27 deletions
|
|
@ -104,6 +104,11 @@ const telemetryLogger: extensionApi.TelemetryLogger = {
|
|||
logError: vi.fn(),
|
||||
} as unknown as extensionApi.TelemetryLogger;
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getPodmanLocationMacMock: vi.fn(),
|
||||
getKrunkitVersionMock: vi.fn(),
|
||||
}));
|
||||
|
||||
// mock ps-list
|
||||
vi.mock('ps-list', async () => {
|
||||
return {
|
||||
|
|
@ -218,13 +223,11 @@ vi.mock('node:os', async () => {
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock('./qemu-helper', async () => {
|
||||
vi.mock('./krunkit-helper', async () => {
|
||||
return {
|
||||
QemuHelper: vi.fn().mockImplementation(() => {
|
||||
KrunkitHelper: vi.fn().mockImplementation(() => {
|
||||
return {
|
||||
getQemuVersion: vi.fn().mockImplementation(() => {
|
||||
return Promise.resolve('1.2.3');
|
||||
}),
|
||||
getKrunkitVersion: mocks.getKrunkitVersionMock,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
|
@ -233,9 +236,7 @@ vi.mock('./podman-binary-location-helper', async () => {
|
|||
return {
|
||||
PodmanBinaryLocationHelper: vi.fn().mockImplementation(() => {
|
||||
return {
|
||||
getPodmanLocationMac: vi.fn().mockImplementation(() => {
|
||||
return Promise.resolve({ source: 'unknown' });
|
||||
}),
|
||||
getPodmanLocationMac: mocks.getPodmanLocationMacMock,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
|
@ -2096,3 +2097,122 @@ test('isLibkrunSupported should return false with previous 5.1.2 version', async
|
|||
const enabled = extension.isLibkrunSupported('5.1.2');
|
||||
expect(enabled).toBeFalsy();
|
||||
});
|
||||
|
||||
test('sendTelemetryRecords with krunkit found', async () => {
|
||||
vi.spyOn(podmanCli, 'getPodmanInstallation').mockResolvedValue({
|
||||
version: '5.1.2',
|
||||
});
|
||||
mocks.getPodmanLocationMacMock.mockResolvedValue({ foundPath: '/opt/podman/bin/podman', source: 'installer' });
|
||||
mocks.getKrunkitVersionMock.mockResolvedValue('1.2.3');
|
||||
|
||||
extension.sendTelemetryRecords(
|
||||
'evt',
|
||||
{
|
||||
provider: 'libkrun',
|
||||
} as Record<string, unknown>,
|
||||
false,
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(telemetryLogger.logUsage).toHaveBeenCalledWith(
|
||||
'evt',
|
||||
expect.objectContaining({
|
||||
krunkitPath: '/opt/podman/bin',
|
||||
krunkitVersion: '1.2.3',
|
||||
podmanCliFoundPath: '/opt/podman/bin/podman',
|
||||
podmanCliSource: 'installer',
|
||||
podmanCliVersion: '5.1.2',
|
||||
provider: 'libkrun',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('sendTelemetryRecords with krunkit not found', async () => {
|
||||
vi.spyOn(podmanCli, 'getPodmanInstallation').mockResolvedValue({
|
||||
version: '5.1.2',
|
||||
});
|
||||
mocks.getPodmanLocationMacMock.mockResolvedValue({ foundPath: '/opt/podman/bin/podman', source: 'installer' });
|
||||
mocks.getKrunkitVersionMock.mockRejectedValue('command not found');
|
||||
|
||||
extension.sendTelemetryRecords(
|
||||
'evt',
|
||||
{
|
||||
provider: 'libkrun',
|
||||
} as Record<string, unknown>,
|
||||
false,
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(telemetryLogger.logUsage).toHaveBeenCalledWith(
|
||||
'evt',
|
||||
expect.objectContaining({
|
||||
errorKrunkitVersion: 'command not found',
|
||||
podmanCliFoundPath: '/opt/podman/bin/podman',
|
||||
podmanCliSource: 'installer',
|
||||
podmanCliVersion: '5.1.2',
|
||||
provider: 'libkrun',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('if a machine stopped is successfully reporting telemetry', async () => {
|
||||
const spyExecPromise = vi
|
||||
.spyOn(extensionApi.process, 'exec')
|
||||
.mockImplementation(() => Promise.resolve({} as extensionApi.RunResult));
|
||||
vi.spyOn(podmanCli, 'getPodmanInstallation').mockResolvedValue({
|
||||
version: '5.1.2',
|
||||
});
|
||||
mocks.getPodmanLocationMacMock.mockResolvedValue({ foundPath: '/opt/podman/bin/podman', source: 'installer' });
|
||||
mocks.getKrunkitVersionMock.mockResolvedValue('1.2.3');
|
||||
await extension.stopMachine(provider, machineInfo);
|
||||
|
||||
// wait a call on telemetryLogger.logUsage
|
||||
while ((telemetryLogger.logUsage as Mock).mock.calls.length === 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
expect(telemetryLogger.logUsage).toBeCalledWith(
|
||||
'podman.machine.stop',
|
||||
expect.objectContaining({
|
||||
krunkitPath: '/opt/podman/bin',
|
||||
krunkitVersion: '1.2.3',
|
||||
podmanCliFoundPath: '/opt/podman/bin/podman',
|
||||
podmanCliSource: 'installer',
|
||||
podmanCliVersion: '5.1.2',
|
||||
provider: 'libkrun',
|
||||
}),
|
||||
);
|
||||
expect(spyExecPromise).toBeCalledWith(podmanCli.getPodmanCli(), ['machine', 'stop', 'name'], expect.anything());
|
||||
});
|
||||
|
||||
test('if a machine stopped is successfully reporting an error in telemetry', async () => {
|
||||
const customError = new Error('Error while starting podman');
|
||||
|
||||
const spyExecPromise = vi.spyOn(extensionApi.process, 'exec').mockImplementation(() => {
|
||||
throw customError;
|
||||
});
|
||||
vi.spyOn(podmanCli, 'getPodmanInstallation').mockResolvedValue({
|
||||
version: '5.1.2',
|
||||
});
|
||||
mocks.getPodmanLocationMacMock.mockResolvedValue({ foundPath: '/opt/podman/bin/podman', source: 'installer' });
|
||||
mocks.getKrunkitVersionMock.mockResolvedValue('1.2.3');
|
||||
await expect(extension.stopMachine(provider, machineInfo)).rejects.toThrow(customError.message);
|
||||
|
||||
// wait a call on telemetryLogger.logUsage
|
||||
while ((telemetryLogger.logUsage as Mock).mock.calls.length === 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
expect(telemetryLogger.logUsage).toBeCalledWith(
|
||||
'podman.machine.stop',
|
||||
expect.objectContaining({
|
||||
krunkitPath: '/opt/podman/bin',
|
||||
krunkitVersion: '1.2.3',
|
||||
podmanCliFoundPath: '/opt/podman/bin/podman',
|
||||
podmanCliSource: 'installer',
|
||||
podmanCliVersion: '5.1.2',
|
||||
error: customError,
|
||||
provider: 'libkrun',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(spyExecPromise).toBeCalledWith(podmanCli.getPodmanCli(), ['machine', 'stop', 'name'], expect.anything());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { compareVersions } from 'compare-versions';
|
|||
|
||||
import { getSocketCompatibility } from './compatibility-mode';
|
||||
import { getDetectionChecks } from './detection-checks';
|
||||
import { KrunkitHelper } from './krunkit-helper';
|
||||
import { PodmanBinaryLocationHelper } from './podman-binary-location-helper';
|
||||
import { PodmanCleanupMacOS } from './podman-cleanup-macos';
|
||||
import { PodmanCleanupWindows } from './podman-cleanup-windows';
|
||||
|
|
@ -37,7 +38,6 @@ import { PodmanConfiguration } from './podman-configuration';
|
|||
import { PodmanInfoHelper } from './podman-info-helper';
|
||||
import { PodmanInstall } from './podman-install';
|
||||
import { PodmanRemoteConnections } from './podman-remote-connections';
|
||||
import { QemuHelper } from './qemu-helper';
|
||||
import { RegistrySetup } from './registry-setup';
|
||||
import {
|
||||
appConfigDir,
|
||||
|
|
@ -89,7 +89,7 @@ const configurationCompatibilityMode = 'setting.dockerCompatibility';
|
|||
let telemetryLogger: extensionApi.TelemetryLogger | undefined;
|
||||
|
||||
const wslHelper = new WslHelper();
|
||||
const qemuHelper = new QemuHelper();
|
||||
const krunkitHelper = new KrunkitHelper();
|
||||
const podmanBinaryHelper = new PodmanBinaryLocationHelper();
|
||||
const podmanInfoHelper = new PodmanInfoHelper();
|
||||
|
||||
|
|
@ -746,10 +746,7 @@ export async function registerProviderFor(
|
|||
await startMachine(provider, podmanConfiguration, machineInfo, context, logger, undefined, false);
|
||||
},
|
||||
stop: async (context, logger): Promise<void> => {
|
||||
await execPodman(['machine', 'stop', machineInfo.name], machineInfo.vmType, {
|
||||
logger: new LoggerDelegator(context, logger),
|
||||
});
|
||||
provider.updateStatus('stopped');
|
||||
await stopMachine(provider, machineInfo, context, logger);
|
||||
},
|
||||
delete: async (logger): Promise<void> => {
|
||||
await execPodman(['machine', 'rm', '-f', machineInfo.name], machineInfo.vmType, {
|
||||
|
|
@ -871,6 +868,7 @@ export async function startMachine(
|
|||
autoStart?: boolean,
|
||||
): Promise<void> {
|
||||
const telemetryRecords: Record<string, unknown> = {};
|
||||
telemetryRecords.provider = machineInfo.vmType;
|
||||
const startTime = performance.now();
|
||||
|
||||
await checkRosettaMacArm(podmanConfiguration);
|
||||
|
|
@ -898,6 +896,31 @@ export async function startMachine(
|
|||
}
|
||||
}
|
||||
|
||||
export async function stopMachine(
|
||||
provider: extensionApi.Provider,
|
||||
machineInfo: MachineInfo,
|
||||
context?: extensionApi.LifecycleContext,
|
||||
logger?: extensionApi.Logger,
|
||||
): Promise<void> {
|
||||
const startTime = performance.now();
|
||||
const telemetryRecords: Record<string, unknown> = {};
|
||||
telemetryRecords.provider = machineInfo.vmType;
|
||||
try {
|
||||
await execPodman(['machine', 'stop', machineInfo.name], machineInfo.vmType, {
|
||||
logger: new LoggerDelegator(context, logger),
|
||||
});
|
||||
provider.updateStatus('stopped');
|
||||
} catch (err: unknown) {
|
||||
telemetryRecords.error = err;
|
||||
throw err;
|
||||
} finally {
|
||||
// send telemetry event
|
||||
const endTime = performance.now();
|
||||
telemetryRecords.duration = endTime - startTime;
|
||||
sendTelemetryRecords('podman.machine.stop', telemetryRecords, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function doHandleError(
|
||||
provider: extensionApi.Provider,
|
||||
machineInfo: MachineInfo,
|
||||
|
|
@ -1764,7 +1787,7 @@ export function isLibkrunSupported(podmanVersion: string): boolean {
|
|||
return isMac() && compareVersions(podmanVersion, PODMAN_MINIMUM_VERSION_FOR_LIBKRUN_SUPPORT) >= 0;
|
||||
}
|
||||
|
||||
function sendTelemetryRecords(
|
||||
export function sendTelemetryRecords(
|
||||
eventName: string,
|
||||
telemetryRecords: Record<string, unknown>,
|
||||
includeMachineStats: boolean,
|
||||
|
|
@ -1784,16 +1807,16 @@ function sendTelemetryRecords(
|
|||
telemetryRecords.hostCpuModel = hostCpus[0].model;
|
||||
|
||||
// on macOS, try to see if podman is coming from brew or from the installer
|
||||
// and display version of qemu
|
||||
// and display version of krunkit
|
||||
if (extensionApi.env.isMac) {
|
||||
let qemuPath: string | undefined;
|
||||
let krunkitPath: string | undefined;
|
||||
|
||||
try {
|
||||
const podmanBinaryResult = await podmanBinaryHelper.getPodmanLocationMac();
|
||||
|
||||
telemetryRecords.podmanCliSource = podmanBinaryResult.source;
|
||||
if (podmanBinaryResult.source === 'installer') {
|
||||
qemuPath = '/opt/podman/qemu/bin';
|
||||
krunkitPath = '/opt/podman/bin';
|
||||
}
|
||||
telemetryRecords.podmanCliFoundPath = podmanBinaryResult.foundPath;
|
||||
if (podmanBinaryResult.error) {
|
||||
|
|
@ -1804,16 +1827,18 @@ function sendTelemetryRecords(
|
|||
console.trace('unable to check from which path podman is coming', error);
|
||||
}
|
||||
|
||||
// add qemu version
|
||||
try {
|
||||
const qemuVersion = await qemuHelper.getQemuVersion(qemuPath);
|
||||
if (qemuPath) {
|
||||
telemetryRecords.qemuPath = qemuPath;
|
||||
if (telemetryRecords.provider === 'libkrun') {
|
||||
// add krunkit version
|
||||
try {
|
||||
const krunkitVersion = await krunkitHelper.getKrunkitVersion(krunkitPath);
|
||||
if (krunkitPath) {
|
||||
telemetryRecords.krunkitPath = krunkitPath;
|
||||
}
|
||||
telemetryRecords.krunkitVersion = krunkitVersion;
|
||||
} catch (error) {
|
||||
console.trace('unable to check krunkit version', error);
|
||||
telemetryRecords.errorKrunkitVersion = error;
|
||||
}
|
||||
telemetryRecords.qemuVersion = qemuVersion;
|
||||
} catch (error) {
|
||||
console.trace('unable to check qemu version', error);
|
||||
telemetryRecords.errorQemuVersion = error;
|
||||
}
|
||||
} else if (extensionApi.env.isWindows) {
|
||||
// try to get wsl version
|
||||
|
|
@ -1856,6 +1881,7 @@ export async function createMachine(
|
|||
let provider: string | undefined;
|
||||
if (params['podman.factory.machine.provider']) {
|
||||
provider = getProviderByLabel(params['podman.factory.machine.provider']);
|
||||
telemetryRecords.provider = provider;
|
||||
}
|
||||
|
||||
// cpus
|
||||
|
|
|
|||
77
extensions/podman/src/krunkit-helper.spec.ts
Normal file
77
extensions/podman/src/krunkit-helper.spec.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 Red Hat, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import * as extensionApi from '@podman-desktop/api';
|
||||
import type { Mock } from 'vitest';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import { KrunkitHelper } from './krunkit-helper';
|
||||
|
||||
let krunkitHelper: KrunkitHelper;
|
||||
|
||||
// mock the API
|
||||
vi.mock('@podman-desktop/api', async () => {
|
||||
return {
|
||||
process: {
|
||||
exec: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
krunkitHelper = new KrunkitHelper();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should grab correct version', async () => {
|
||||
const output = `krunkit 0.1.2
|
||||
`;
|
||||
|
||||
(extensionApi.process.exec as Mock).mockReturnValue({
|
||||
stdout: output,
|
||||
} as extensionApi.RunResult);
|
||||
|
||||
// use a specific arch for the test
|
||||
const version = await krunkitHelper.getKrunkitVersion();
|
||||
|
||||
expect(version).toBe('0.1.2');
|
||||
|
||||
// expect called with qemu-system-aarch64 (as it's arm64)
|
||||
expect(extensionApi.process.exec).toHaveBeenCalledWith('krunkit', ['--version'], undefined);
|
||||
});
|
||||
|
||||
test('should grab correct version using a given path', async () => {
|
||||
const output = `krunkit 0.1.2
|
||||
`;
|
||||
|
||||
(extensionApi.process.exec as Mock).mockReturnValue({
|
||||
stdout: output,
|
||||
} as extensionApi.RunResult);
|
||||
|
||||
const fakePath = '/my-dummy-path';
|
||||
|
||||
const version = await krunkitHelper.getKrunkitVersion(fakePath);
|
||||
|
||||
expect(version).toBe('0.1.2');
|
||||
|
||||
expect(extensionApi.process.exec).toHaveBeenCalledWith('krunkit', ['--version'], {
|
||||
env: {
|
||||
PATH: fakePath,
|
||||
},
|
||||
});
|
||||
});
|
||||
39
extensions/podman/src/krunkit-helper.ts
Normal file
39
extensions/podman/src/krunkit-helper.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2024 Red Hat, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import * as extensionApi from '@podman-desktop/api';
|
||||
|
||||
export class KrunkitHelper {
|
||||
async getKrunkitVersion(krunkitPath?: string): Promise<string | undefined> {
|
||||
const binaryName = 'krunkit';
|
||||
// grab output of the krunkit CLI
|
||||
let env;
|
||||
if (krunkitPath) {
|
||||
env = {
|
||||
env: {
|
||||
PATH: krunkitPath,
|
||||
},
|
||||
};
|
||||
}
|
||||
const { stdout } = await extensionApi.process.exec(binaryName, ['--version'], env);
|
||||
// stdout is like krunkit 0.1.2
|
||||
|
||||
// extract 0.1.2 from the string krunkit 0.1.2
|
||||
return RegExp(/krunkit ([0-9.]+)/).exec(stdout)?.[1];
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue