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:
Philippe Martin 2024-09-05 10:29:05 +02:00 committed by GitHub
parent d255d95bb3
commit 425afb11da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 289 additions and 27 deletions

View file

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

View file

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

View 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,
},
});
});

View 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];
}
}