Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Upload model on podman machine - Qemu #439

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 6 additions & 19 deletions packages/backend/src/managers/applicationManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,8 @@ const mocks = vi.hoisted(() => {
getImageInspectMock: vi.fn(),
createPodMock: vi.fn(),
createContainerMock: vi.fn(),
replicatePodmanContainerMock: vi.fn(),
startContainerMock: vi.fn(),
startPod: vi.fn(),
deleteContainerMock: vi.fn(),
inspectContainerMock: vi.fn(),
logUsageMock: vi.fn(),
logErrorMock: vi.fn(),
Expand Down Expand Up @@ -107,10 +105,8 @@ vi.mock('@podman-desktop/api', () => ({
getImageInspect: mocks.getImageInspectMock,
createPod: mocks.createPodMock,
createContainer: mocks.createContainerMock,
replicatePodmanContainer: mocks.replicatePodmanContainerMock,
startContainer: mocks.startContainerMock,
startPod: mocks.startPod,
deleteContainer: mocks.deleteContainerMock,
inspectContainer: mocks.inspectContainerMock,
pullImage: mocks.pullImageMock,
stopContainer: mocks.stopContainerMock,
Expand Down Expand Up @@ -259,6 +255,7 @@ describe('pullApplication', () => {
});
mocks.listPodsMock.mockResolvedValue([]);
vi.spyOn(modelsManager, 'isModelOnDisk').mockReturnValue(false);
vi.spyOn(modelsManager, 'uploadModelToPodmanMachine').mockResolvedValue('path');
mocks.performDownloadMock.mockResolvedValue('path');
const recipe: Recipe = {
id: 'recipe1',
Expand Down Expand Up @@ -321,6 +318,7 @@ describe('pullApplication', () => {
});
mocks.listPodsMock.mockResolvedValue([]);
vi.spyOn(modelsManager, 'isModelOnDisk').mockReturnValue(false);
vi.spyOn(modelsManager, 'uploadModelToPodmanMachine').mockResolvedValue('path');
mocks.performDownloadMock.mockResolvedValue('path');
const recipe: Recipe = {
id: 'recipe1',
Expand Down Expand Up @@ -349,6 +347,7 @@ describe('pullApplication', () => {
});
mocks.listPodsMock.mockResolvedValue([]);
vi.spyOn(modelsManager, 'isModelOnDisk').mockReturnValue(true);
vi.spyOn(modelsManager, 'uploadModelToPodmanMachine').mockResolvedValue('path');
vi.spyOn(modelsManager, 'getLocalModelPath').mockReturnValue('path');
const recipe: Recipe = {
id: 'recipe1',
Expand Down Expand Up @@ -1007,35 +1006,23 @@ describe('createAndAddContainersToPod', () => {
modelService: false,
ports: ['8080'],
};
test('check that after the creation and copy inside the pod, the container outside the pod is actually deleted', async () => {
test('check that container is created inside the pod', async () => {
mocks.createContainerMock.mockResolvedValue({
id: 'container-1',
});
vi.spyOn(manager, 'getRandomName').mockReturnValue('name');
await manager.createAndAddContainersToPod(pod, [imageInfo1], 'path');
expect(mocks.createContainerMock).toBeCalledWith('engine', {
Image: 'id',
name: 'name',
Detach: true,
HostConfig: {
AutoRemove: true,
},
Env: [],
start: false,
pod: pod.Id,
});
expect(mocks.replicatePodmanContainerMock).toBeCalledWith(
{
id: 'container-1',
engineId: 'engine',
},
{
engineId: 'engine',
},
{
pod: 'id',
name: 'name',
},
);
expect(mocks.deleteContainerMock).toBeCalledWith('engine', 'container-1');
});
});

Expand Down
41 changes: 17 additions & 24 deletions packages/backend/src/managers/applicationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,13 @@
const configAndFilteredContainers = this.getConfigAndFilterContainers(recipe.config, localFolder);

// get model by downloading it or retrieving locally
const modelPath = await this.modelsManager.downloadModel(model, {
let modelPath = await this.modelsManager.downloadModel(model, {
'recipe-id': recipe.id,
'model-id': model.id,
});

// upload model to podman machine if user system is supported
modelPath = await this.modelsManager.uploadModelToPodmanMachine(model, modelPath, {
'recipe-id': recipe.id,
'model-id': model.id,
});
Expand Down Expand Up @@ -258,6 +264,7 @@
Target: `/${modelName}`,
Source: modelPath,
Type: 'bind',
Mode: 'Z',
},
],
};
Expand All @@ -273,39 +280,25 @@
envs = [`MODEL_ENDPOINT=${endPoint}`];
}
}
const createdContainer = await containerEngine.createContainer(podInfo.engineId, {
const podifiedName = this.getRandomName(`${image.appName}-podified`);
await containerEngine.createContainer(podInfo.engineId, {
Image: image.id,
name: podifiedName,
Detach: true,
HostConfig: hostConfig,
Env: envs,
start: false,
pod: podInfo.Id,
});
containers.push({
name: podifiedName,
modelService: image.modelService,
ports: image.ports,
});

// now, for each container, put it in the pod
if (createdContainer) {
const podifiedName = this.getRandomName(`${image.appName}-podified`);
await containerEngine.replicatePodmanContainer(
{
id: createdContainer.id,
engineId: podInfo.engineId,
},
{ engineId: podInfo.engineId },
{ pod: podInfo.Id, name: podifiedName },
);
containers.push({
name: podifiedName,
modelService: image.modelService,
ports: image.ports,
});
// remove the external container
await containerEngine.deleteContainer(podInfo.engineId, createdContainer.id);
} else {
throw new Error(`failed at creating container for image ${image.id}`);
}
}),
);
return containers;
}

Check failure on line 301 in packages/backend/src/managers/applicationManager.ts

View workflow job for this annotation

GitHub Actions / linter, formatters and unit tests / windows-2022

Object literal may only specify known properties, and 'pod' does not exist in type 'ContainerCreateOptions'.

Check failure on line 301 in packages/backend/src/managers/applicationManager.ts

View workflow job for this annotation

GitHub Actions / linter, formatters and unit tests / ubuntu-22.04

Object literal may only specify known properties, and 'pod' does not exist in type 'ContainerCreateOptions'.

Check failure on line 301 in packages/backend/src/managers/applicationManager.ts

View workflow job for this annotation

GitHub Actions / linter, formatters and unit tests / macos-12

Object literal may only specify known properties, and 'pod' does not exist in type 'ContainerCreateOptions'.

async createPod(recipe: Recipe, model: ModelInfo, images: ImageInfo[]): Promise<ApplicationPodInfo> {
// find the exposed port of the sample app so we can open its ports on the new pod
Expand Down
56 changes: 52 additions & 4 deletions packages/backend/src/managers/modelsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ import { MSG_NEW_MODELS_STATE } from '@shared/Messages';
import type { CatalogManager } from './catalogManager';
import type { ModelInfo } from '@shared/src/models/IModelInfo';
import * as podmanDesktopApi from '@podman-desktop/api';
import { Downloader, type DownloadEvent, isCompletionEvent, isProgressEvent } from '../utils/downloader';
import { Downloader } from '../utils/downloader';
import type { TaskRegistry } from '../registries/TaskRegistry';
import type { Task } from '@shared/src/models/ITask';
import type { ProgressiveEvent } from '../utils/progressiveEvent';
import { isCompletionProgressiveEvent, isProgressProgressiveEvent } from '../utils/progressiveEvent';
import { Uploader } from '../models/uploader';

export class ModelsManager implements Disposable {
#modelsDir: string;
Expand Down Expand Up @@ -201,11 +204,11 @@ export class ModelsManager implements Disposable {
const downloader = new Downloader(model.url, target);

// Capture downloader events
downloader.onEvent((event: DownloadEvent) => {
if (isProgressEvent(event)) {
downloader.onEvent((event: ProgressiveEvent) => {
if (isProgressProgressiveEvent(event)) {
task.state = 'loading';
task.progress = event.value;
} else if (isCompletionEvent(event)) {
} else if (isCompletionProgressiveEvent(event)) {
// status error or canceled
if (event.status === 'error' || event.status === 'canceled') {
task.state = 'error';
Expand Down Expand Up @@ -235,4 +238,49 @@ export class ModelsManager implements Disposable {
await downloader.perform();
return target;
}

async uploadModelToPodmanMachine(
model: ModelInfo,
localModelPath: string,
labels?: { [key: string]: string },
): Promise<string> {
const task: Task = this.taskRegistry.createTask(`Uploading model ${model.name}`, 'loading', {
...labels,
'model-uploading': model.id,
});

const uploader = new Uploader(localModelPath);
uploader.onEvent((event: ProgressiveEvent) => {
if (isProgressProgressiveEvent(event)) {
task.state = 'loading';
task.progress = event.value;
} else if (isCompletionProgressiveEvent(event)) {
// status error or canceled
if (event.status === 'error' || event.status === 'canceled') {
task.state = 'error';
task.progress = undefined;
task.error = event.message;

// telemetry usage
this.telemetry.logError('model.upload', {
'model.id': model.id,
message: 'error uploading model',
error: event.message,
durationSeconds: event.duration,
});
} else {
task.state = 'success';
task.progress = 100;

// telemetry usage
this.telemetry.logUsage('model.upload', { 'model.id': model.id, durationSeconds: event.duration });
}
}

this.taskRegistry.updateTask(task); // update task
});

// perform download
return await uploader.perform();
}
}
31 changes: 17 additions & 14 deletions packages/backend/src/managers/playground.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const mocks = vi.hoisted(() => ({
listContainers: vi.fn(),
logUsage: vi.fn(),
logError: vi.fn(),
getFirstRunningPodmanConnectionMock: vi.fn(),
}));

vi.mock('@podman-desktop/api', async () => {
Expand All @@ -51,6 +52,12 @@ vi.mock('@podman-desktop/api', async () => {
};
});

vi.mock('../utils/podman', () => {
return {
getFirstRunningPodmanConnection: mocks.getFirstRunningPodmanConnectionMock,
};
});

const containerRegistryMock = {
subscribe: mocks.containerRegistrySubscribeMock,
} as unknown as ContainerRegistry;
Expand Down Expand Up @@ -99,14 +106,12 @@ test('startPlayground should fail if no provider', async () => {

test('startPlayground should download image if not present then create container', async () => {
mocks.postMessage.mockResolvedValue(undefined);
mocks.getContainerConnections.mockReturnValue([
{
connection: {
type: 'podman',
status: () => 'started',
},
mocks.getFirstRunningPodmanConnectionMock.mockReturnValue({
connection: {
type: 'podman',
status: () => 'started',
},
]);
});
vi.spyOn(manager, 'selectImage')
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce({
Expand Down Expand Up @@ -157,14 +162,12 @@ test('stopPlayground should fail if no playground is running', async () => {

test('stopPlayground should stop a started playground', async () => {
mocks.postMessage.mockResolvedValue(undefined);
mocks.getContainerConnections.mockReturnValue([
{
connection: {
type: 'podman',
status: () => 'started',
},
mocks.getFirstRunningPodmanConnectionMock.mockReturnValue({
connection: {
type: 'podman',
status: () => 'started',
},
]);
});
vi.spyOn(manager, 'selectImage').mockResolvedValue({
Id: 'image1',
engineId: 'engine1',
Expand Down
20 changes: 3 additions & 17 deletions packages/backend/src/managers/playground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,7 @@
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import {
containerEngine,
type Webview,
type ImageInfo,
type ProviderContainerConnection,
provider,
type TelemetryLogger,
} from '@podman-desktop/api';
import { containerEngine, type Webview, type ImageInfo, type TelemetryLogger } from '@podman-desktop/api';

import path from 'node:path';
import { getFreePort } from '../utils/ports';
Expand All @@ -35,6 +28,7 @@ import type { PodmanConnection } from './podmanConnection';
import OpenAI from 'openai';
import { getDurationSecondsSince, timeout } from '../utils/utils';
import type { ModelInfo } from '@shared/src/models/IModelInfo';
import { getFirstRunningPodmanConnection } from '../utils/podman';

export const LABEL_MODEL_ID = 'ai-studio-model-id';
export const LABEL_MODEL_PORT = 'ai-studio-model-port';
Expand All @@ -44,14 +38,6 @@ const PLAYGROUND_IMAGE = 'quay.io/bootsy/playground:v0';

const STARTING_TIME_MAX = 3600 * 1000;

function findFirstProvider(): ProviderContainerConnection | undefined {
const engines = provider
.getContainerConnections()
.filter(connection => connection.connection.type === 'podman')
.filter(connection => connection.connection.status() === 'started');
return engines.length > 0 ? engines[0] : undefined;
}

export class PlayGroundManager {
private queryIdCounter = 0;

Expand Down Expand Up @@ -171,7 +157,7 @@ export class PlayGroundManager {

this.setPlaygroundStatus(modelId, 'starting');

const connection = findFirstProvider();
const connection = getFirstRunningPodmanConnection();
if (!connection) {
const error = 'Unable to find an engine to start playground';
this.setPlaygroundError(modelId, error);
Expand Down
25 changes: 14 additions & 11 deletions packages/backend/src/managers/podmanConnection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { PodmanConnection } from './podmanConnection';
import type { RegisterContainerConnectionEvent, UpdateContainerConnectionEvent } from '@podman-desktop/api';

const mocks = vi.hoisted(() => ({
getContainerConnections: vi.fn(),
getFirstRunningPodmanConnectionMock: vi.fn(),
onDidRegisterContainerConnection: vi.fn(),
onDidUpdateContainerConnection: vi.fn(),
}));
Expand All @@ -30,23 +30,26 @@ vi.mock('@podman-desktop/api', async () => {
return {
provider: {
onDidRegisterContainerConnection: mocks.onDidRegisterContainerConnection,
getContainerConnections: mocks.getContainerConnections,
onDidUpdateContainerConnection: mocks.onDidUpdateContainerConnection,
},
};
});

test('startupSubscribe should execute immediately if provider already registered', () => {
vi.mock('../utils/podman', () => {
return {
getFirstRunningPodmanConnection: mocks.getFirstRunningPodmanConnectionMock,
};
});

test('startupSubscribe should execute immediately if provider already registered', async () => {
const manager = new PodmanConnection();
// one provider is already registered
mocks.getContainerConnections.mockReturnValue([
{
connection: {
type: 'podman',
status: () => 'started',
},
mocks.getFirstRunningPodmanConnectionMock.mockReturnValue({
connection: {
type: 'podman',
status: () => 'started',
},
]);
});
mocks.onDidRegisterContainerConnection.mockReturnValue({
dispose: vi.fn,
});
Expand All @@ -61,7 +64,7 @@ test('startupSubscribe should execute when provider is registered', async () =>
const manager = new PodmanConnection();

// no provider is already registered
mocks.getContainerConnections.mockReturnValue([]);
mocks.getFirstRunningPodmanConnectionMock.mockReturnValue(undefined);
mocks.onDidRegisterContainerConnection.mockImplementation((f: (e: RegisterContainerConnectionEvent) => void) => {
setTimeout(() => {
f({
Expand Down
Loading
Loading