Skip to content

Commit

Permalink
feat: show warning if no machine is running or there are not enough r…
Browse files Browse the repository at this point in the history
…esources (#227) (#856)

* feat: show warning if no machine is running or there are not enough resources (#227)

Signed-off-by: lstocchi <lstocchi@redhat.com>
Signed-off-by: Jeff MAURY <jmaury@redhat.com>
Co-authored-by: Jeff MAURY <jmaury@redhat.com>
  • Loading branch information
lstocchi and jeffmaury committed Apr 12, 2024
1 parent 1f4223a commit f59969d
Show file tree
Hide file tree
Showing 15 changed files with 946 additions and 11 deletions.
2 changes: 1 addition & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"xml-js": "^1.6.11"
},
"devDependencies": {
"@podman-desktop/api": "^1.9.0",
"@podman-desktop/api": "0.0.202404101645-5d46ba5",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20",
"@types/postman-collection": "^3.5.10",
Expand Down
47 changes: 45 additions & 2 deletions packages/backend/src/studio-api-impl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import content from './tests/ai-test.json';
import type { ApplicationManager } from './managers/applicationManager';
import { StudioApiImpl } from './studio-api-impl';
import type { InferenceManager } from './managers/inference/inferenceManager';
import { type TelemetryLogger, type Webview, window, EventEmitter } from '@podman-desktop/api';
import type { ProviderContainerConnection, TelemetryLogger, Webview } from '@podman-desktop/api';
import { window, EventEmitter, navigation } from '@podman-desktop/api';
import { CatalogManager } from './managers/catalogManager';
import type { ModelsManager } from './managers/modelsManager';
import { timeout } from './utils/utils';
Expand All @@ -32,10 +33,11 @@ import { LocalRepositoryRegistry } from './registries/LocalRepositoryRegistry';
import type { Recipe } from '@shared/src/models/IRecipe';
import type { PlaygroundV2Manager } from './managers/playgroundV2Manager';
import type { SnippetManager } from './managers/SnippetManager';
import type { ModelInfo } from '@shared/src/models/IModelInfo';
import type { ModelCheckerInfo, ModelInfo } from '@shared/src/models/IModelInfo';
import type { CancellationTokenRegistry } from './registries/CancellationTokenRegistry';
import path from 'node:path';
import type { LocalModelImportInfo } from '@shared/src/models/ILocalModelInfo';
import * as podman from './utils/podman';

vi.mock('./ai.json', () => {
return {
Expand Down Expand Up @@ -85,6 +87,10 @@ vi.mock('@podman-desktop/api', async () => {
env: {
openExternal: mocks.openExternalMock,
},
navigation: {
navigateToResources: vi.fn(),
navigateToEditProviderContainerConnection: vi.fn(),
},
};
});

Expand Down Expand Up @@ -313,3 +319,40 @@ test('Expect checkInvalidModels to returns an array with the invalid values', as
const result = await studioApiImpl.checkInvalidModels(['path/file.gguf', 'path1/file.gguf', 'path2/file.gguf']);
expect(result).toStrictEqual(['path/file.gguf', 'path2/file.gguf']);
});

test('navigateToResources should call navigation.navigateToResources', async () => {
const navigationSpy = vi.spyOn(navigation, 'navigateToResources');
await studioApiImpl.navigateToResources();
await timeout(0);
expect(navigationSpy).toHaveBeenCalled();
});

test('navigateToEditConnectionProvider should call navigation.navigateToEditProviderContainerConnection', async () => {
const connection: ProviderContainerConnection = {
providerId: 'id',
connection: {
endpoint: {
socketPath: '/path',
},
name: 'name',
type: 'podman',
status: vi.fn(),
},
};
vi.spyOn(podman, 'getPodmanConnection').mockReturnValue(connection);
const navigationSpy = vi.spyOn(navigation, 'navigateToEditProviderContainerConnection');
await studioApiImpl.navigateToEditConnectionProvider('connection');
await timeout(0);
expect(navigationSpy).toHaveBeenCalledWith(connection);
});

test('checkContainerConnectionStatusAndResources should call podman.checkContainerConnectionStatusAndResources', async () => {
const checkContainerSpy = vi.spyOn(podman, 'checkContainerConnectionStatusAndResources');
const modelInfo: ModelCheckerInfo = {
memoryNeeded: 1000,
context: 'inference',
};
await studioApiImpl.checkContainerConnectionStatusAndResources(modelInfo);
await timeout(0);
expect(checkContainerSpy).toHaveBeenCalledWith(modelInfo);
});
23 changes: 22 additions & 1 deletion packages/backend/src/studio-api-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import type { StudioAPI } from '@shared/src/StudioAPI';
import type { ApplicationManager } from './managers/applicationManager';
import type { ModelInfo } from '@shared/src/models/IModelInfo';
import type { ModelCheckerInfo, ModelInfo } from '@shared/src/models/IModelInfo';
import * as podmanDesktopApi from '@podman-desktop/api';

import type { CatalogManager } from './managers/catalogManager';
Expand All @@ -43,6 +43,8 @@ import type { Language } from 'postman-code-generators';
import type { ModelOptions } from '@shared/src/models/IModelOptions';
import type { CancellationTokenRegistry } from './registries/CancellationTokenRegistry';
import type { LocalModelImportInfo } from '@shared/src/models/ILocalModelInfo';
import { checkContainerConnectionStatusAndResources, getPodmanConnection } from './utils/podman';
import type { ContainerConnectionInfo } from '@shared/src/models/IContainerConnectionInfo';

interface PortQuickPickItem extends podmanDesktopApi.QuickPickItem {
port: number;
Expand Down Expand Up @@ -250,6 +252,21 @@ export class StudioApiImpl implements StudioAPI {
return podmanDesktopApi.navigation.navigateToPod(pod.kind, pod.Name, pod.engineId);
}

async navigateToResources(): Promise<void> {
// navigateToResources is only vailable from desktop 1.10
if (podmanDesktopApi.navigation.navigateToResources) {
return podmanDesktopApi.navigation.navigateToResources();
}
}

async navigateToEditConnectionProvider(connectionName: string): Promise<void> {
// navigateToEditProviderContainerConnection is only vailable from desktop 1.10
if (podmanDesktopApi.navigation.navigateToEditProviderContainerConnection) {
const connection = getPodmanConnection(connectionName);
return podmanDesktopApi.navigation.navigateToEditProviderContainerConnection(connection);
}
}

async getApplicationsState(): Promise<ApplicationState[]> {
return this.applicationManager.getApplicationsState();
}
Expand Down Expand Up @@ -467,4 +484,8 @@ export class StudioApiImpl implements StudioAPI {
copyToClipboard(content: string): Promise<void> {
return podmanDesktopApi.env.clipboard.writeText(content);
}

async checkContainerConnectionStatusAndResources(modelInfo: ModelCheckerInfo): Promise<ContainerConnectionInfo> {
return checkContainerConnectionStatusAndResources(modelInfo);
}
}
181 changes: 181 additions & 0 deletions packages/backend/src/utils/podman.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ vi.mock('@podman-desktop/api', () => {
configuration: {
getConfiguration: () => config,
},
containerEngine: {
info: vi.fn(),
},
navigation: {
navigateToResources: vi.fn(),
},
provider: {
getContainerConnections: mocks.getContainerConnectionsMock,
},
Expand Down Expand Up @@ -278,3 +284,178 @@ describe('isQEMUMachine', () => {
expect(isQEMU).toBeFalsy();
});
});

describe('getPodmanConnection', () => {
test('throw error if there is no podman connection with name', () => {
mocks.getContainerConnectionsMock.mockReturnValue([
{
connection: {
name: 'Podman Machine',
status: () => 'started',
endpoint: {
socketPath: '/endpoint.sock',
},
type: 'podman',
},
providerId: 'podman',
},
]);
expect(() => utils.getPodmanConnection('sample')).toThrowError('no podman connection found with name sample');
});
test('return connection with specified name', () => {
mocks.getContainerConnectionsMock.mockReturnValue([
{
connection: {
name: 'Podman Machine',
status: () => 'started',
endpoint: {
socketPath: '/endpoint.sock',
},
type: 'podman',
},
providerId: 'podman',
},
]);
const engine = utils.getPodmanConnection('Podman Machine');
expect(engine).toBeDefined();
expect(engine.providerId).equals('podman');
expect(engine.connection.name).equals('Podman Machine');
});
});

describe('checkContainerConnectionStatusAndResources', () => {
const engineInfo: podmanDesktopApi.ContainerEngineInfo = {
engineId: 'engineId',
engineName: 'enginerName',
engineType: 'podman',
};
test('return noMachineInfo if there is no running podman connection', async () => {
vi.spyOn(utils, 'getFirstRunningPodmanConnection').mockReturnValue(undefined);
const result = await utils.checkContainerConnectionStatusAndResources({
memoryNeeded: 10,
context: 'inference',
});
expect(result).toStrictEqual({
status: 'no-machine',
canRedirect: true,
});
});
test('return noMachineInfo if we are not able to retrieve any info about the podman connection', async () => {
mocks.getContainerConnectionsMock.mockReturnValue([
{
connection: {
name: 'Podman Machine',
status: () => 'started',
endpoint: {
socketPath: '/endpoint.sock',
},
type: 'podman',
},
providerId: 'podman',
},
]);
vi.mocked(podmanDesktopApi.containerEngine.info).mockResolvedValue(undefined);
const result = await utils.checkContainerConnectionStatusAndResources({
memoryNeeded: 10,
context: 'inference',
});
expect(result).toStrictEqual({
status: 'no-machine',
canRedirect: true,
});
});
test('return lowResourceMachineInfo if the podman connection has not enough cpus', async () => {
engineInfo.cpus = 3;
engineInfo.memory = 20;
engineInfo.memoryUsed = 0;
mocks.getContainerConnectionsMock.mockReturnValue([
{
connection: {
name: 'Podman Machine',
status: () => 'started',
endpoint: {
socketPath: '/endpoint.sock',
},
type: 'podman',
},
providerId: 'podman',
},
]);
vi.mocked(podmanDesktopApi.containerEngine.info).mockResolvedValue(engineInfo);
const result = await utils.checkContainerConnectionStatusAndResources({
memoryNeeded: 10,
context: 'inference',
});
expect(result).toStrictEqual({
name: 'Podman Machine',
cpus: 3,
memoryIdle: 20,
cpusExpected: 4,
memoryExpected: 11,
status: 'low-resources',
canEdit: false,
canRedirect: true,
});
});
test('return lowResourceMachineInfo if the podman connection has not enough memory', async () => {
engineInfo.cpus = 12;
engineInfo.memory = 20;
engineInfo.memoryUsed = 15;
mocks.getContainerConnectionsMock.mockReturnValue([
{
connection: {
name: 'Podman Machine',
status: () => 'started',
endpoint: {
socketPath: '/endpoint.sock',
},
type: 'podman',
},
providerId: 'podman',
},
]);
vi.mocked(podmanDesktopApi.containerEngine.info).mockResolvedValue(engineInfo);
const result = await utils.checkContainerConnectionStatusAndResources({
memoryNeeded: 10,
context: 'inference',
});
expect(result).toStrictEqual({
name: 'Podman Machine',
cpus: 12,
memoryIdle: 5,
cpusExpected: 4,
memoryExpected: 11,
status: 'low-resources',
canEdit: false,
canRedirect: true,
});
});
test('return runningMachineInfo if the podman connection has enough resources', async () => {
engineInfo.cpus = 12;
engineInfo.memory = 20;
engineInfo.memoryUsed = 0;
mocks.getContainerConnectionsMock.mockReturnValue([
{
connection: {
name: 'Podman Machine',
status: () => 'started',
endpoint: {
socketPath: '/endpoint.sock',
},
type: 'podman',
},
providerId: 'podman',
},
]);
vi.spyOn(podmanDesktopApi.containerEngine, 'info').mockResolvedValue(engineInfo);
const result = await utils.checkContainerConnectionStatusAndResources({
memoryNeeded: 10,
context: 'inference',
});
expect(result).toStrictEqual({
name: 'Podman Machine',
status: 'running',
canRedirect: true,
});
});
});

0 comments on commit f59969d

Please sign in to comment.