Skip to content

Commit

Permalink
send telemetry related to krunkit (#8680)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
feloy authored Sep 5, 2024
1 parent d255d95 commit 425afb1
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 27 deletions.
136 changes: 128 additions & 8 deletions extensions/podman/src/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
};
}),
};
Expand All @@ -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,
};
}),
};
Expand Down Expand Up @@ -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());
});
64 changes: 45 additions & 19 deletions extensions/podman/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions extensions/podman/src/krunkit-helper.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
});
Loading

0 comments on commit 425afb1

Please sign in to comment.