Skip to content

Commit

Permalink
feat: import recipe
Browse files Browse the repository at this point in the history
Signed-off-by: axel7083 <[email protected]>
  • Loading branch information
axel7083 committed May 21, 2024
1 parent 2289465 commit da49abb
Show file tree
Hide file tree
Showing 12 changed files with 491 additions and 45 deletions.
152 changes: 148 additions & 4 deletions packages/backend/src/managers/applicationManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,8 +486,8 @@ describe('getConfiguration', () => {
localRepositoryRegistry,
);
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
expect(() => manager.getConfiguration('config', 'local')).toThrowError(
`The file located at ${path.join('local', 'config', CONFIG_FILENAME)} does not exist.`,
expect(() => manager.getConfiguration('local')).toThrowError(
`The file located at ${path.join('local', CONFIG_FILENAME)} does not exist.`,
);
});

Expand Down Expand Up @@ -522,8 +522,8 @@ describe('getConfiguration', () => {
};
mocks.parseYamlFileMock.mockReturnValue(aiConfig);

const result = manager.getConfiguration('config', 'local');
expect(result.path).toEqual(path.join('local', 'config', CONFIG_FILENAME));
const result = manager.getConfiguration('local');
expect(result.path).toEqual(path.join('local', CONFIG_FILENAME));
expect(result.aiConfig).toEqual(aiConfig);
});
});
Expand All @@ -532,6 +532,8 @@ describe('filterContainers', () => {
test('return empty array when no container fit the system', () => {
const aiConfig: AIConfig = {
application: {
name: 'dummy-name',
type: 'dummy-type',
containers: [
{
name: 'container2',
Expand Down Expand Up @@ -564,6 +566,8 @@ describe('filterContainers', () => {
test('return one container when only one fit the system', () => {
const aiConfig: AIConfig = {
application: {
name: 'dummy-name',
type: 'dummy-type',
containers: [
{
name: 'container1',
Expand Down Expand Up @@ -631,6 +635,8 @@ describe('filterContainers', () => {
];
const aiConfig: AIConfig = {
application: {
name: 'dummy-name',
type: 'dummy-type',
containers: containerConfig,
},
};
Expand Down Expand Up @@ -1474,3 +1480,141 @@ describe('getImageTag', () => {
expect(state[0].health).toEqual('healthy');
});
});

describe('getRecipeLocalFolder', () => {
const appUserDirectory = '/home/user/aistudio';
const processCheckoutMock = vi.fn();

test('recipe with git repository should checkout', async () => {
const manager = new ApplicationManager(
appUserDirectory,
{
processCheckout: processCheckoutMock,
isGitInstalled: () => true,
} as unknown as GitManager,
taskRegistry,
{} as Webview,
{} as PodmanConnection,
{} as CatalogManager,
{} as unknown as ModelsManager,
telemetryLogger,
localRepositoryRegistry,
);

const local = await manager.getRecipeLocalFolder({
id: 'dummy-recipe-id',
name: 'dummy-recipe-name',
basedir: undefined,
repository: 'dummy-repository',
readme: 'dummy-readme',
description: '',
categories: [],
}, {id: 'dummy-model-id'} as unknown as ModelInfo);

const targetDir = path.join(appUserDirectory, 'dummy-recipe-id');
expect(processCheckoutMock).toHaveBeenCalledWith({
repository: 'dummy-repository',
ref: undefined,
targetDirectory: targetDir,
});

expect(local).toBe(targetDir);
});

test('recipe with git repository and basedir should return joined path', async () => {
const manager = new ApplicationManager(
appUserDirectory,
{
processCheckout: processCheckoutMock,
isGitInstalled: () => true,
} as unknown as GitManager,
taskRegistry,
{} as Webview,
{} as PodmanConnection,
{} as CatalogManager,
{} as unknown as ModelsManager,
telemetryLogger,
localRepositoryRegistry,
);

const local = await manager.getRecipeLocalFolder({
id: 'dummy-recipe-id',
name: 'dummy-recipe-name',
basedir: 'relative-path',
repository: 'dummy-repository',
readme: 'dummy-readme',
description: '',
categories: [],
}, {id: 'dummy-model-id'} as unknown as ModelInfo);

const targetDir = path.join(appUserDirectory, 'dummy-recipe-id', 'relative-path');
expect(local).toBe(targetDir);
});

test('recipe without git repository should return basedir path', async () => {
const isAbsoluteMock = vi.spyOn(path, 'isAbsolute');
isAbsoluteMock.mockReturnValue(true);

const manager = new ApplicationManager(
appUserDirectory,
{
processCheckout: processCheckoutMock,
isGitInstalled: () => true,
} as unknown as GitManager,
taskRegistry,
{} as Webview,
{} as PodmanConnection,
{} as CatalogManager,
{} as unknown as ModelsManager,
telemetryLogger,
localRepositoryRegistry,
);

const local = await manager.getRecipeLocalFolder({
id: 'dummy-recipe-id',
name: 'dummy-recipe-name',
basedir: 'absolute-path',
repository: undefined,
readme: 'dummy-readme',
description: '',
categories: [],
}, {id: 'dummy-model-id'} as unknown as ModelInfo);

expect(processCheckoutMock).not.toHaveBeenCalled();
expect(isAbsoluteMock).toHaveBeenCalled();

expect(local).toBe('absolute-path');
});

test('recipe without git repository and not absolute basedir should throw error', async () => {
const isAbsoluteMock = vi.spyOn(path, 'isAbsolute');
isAbsoluteMock.mockReturnValue(false);

const manager = new ApplicationManager(
appUserDirectory,
{
processCheckout: processCheckoutMock,
isGitInstalled: () => true,
} as unknown as GitManager,
taskRegistry,
{} as Webview,
{} as PodmanConnection,
{} as CatalogManager,
{} as unknown as ModelsManager,
telemetryLogger,
localRepositoryRegistry,
);

expect(async () => {

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

View workflow job for this annotation

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

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

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

View workflow job for this annotation

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

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

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

View workflow job for this annotation

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

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
await manager.getRecipeLocalFolder({
id: 'dummy-recipe-id',
name: 'dummy-recipe-name',
basedir: 'absolute-path',
repository: undefined,
readme: 'dummy-readme',
description: '',
categories: [],
}, {id: 'dummy-model-id'} as unknown as ModelInfo);
}).rejects.toThrowError('recipe is malformed: either a repository or an absolute basedir must be defined.');
});
});
51 changes: 33 additions & 18 deletions packages/backend/src/managers/applicationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import type { Recipe } from '@shared/src/models/IRecipe';
import type { GitCloneInfo, GitManager } from './gitManager';
import fs from 'fs';
import * as path from 'node:path';
import path from 'node:path';
import { containerEngine, Disposable } from '@podman-desktop/api';
import type {
BuildImageOptions,
Expand Down Expand Up @@ -114,10 +114,14 @@ export class ApplicationManager extends Publisher<ApplicationState[]> implements
return this.startApplication(recipe, model);
}

async startApplication(recipe: Recipe, model: ModelInfo) {
// const recipeStatus = this.recipeStatusRegistry.
const startTime = performance.now();
try {
/**
* The local folder is the path to the directory containing the CONFIG_FILENAME (ai-lab.yaml)
*
* @return the absolute path to the folder with the CONFIG_FILENAME file in it.
*/
async getRecipeLocalFolder(recipe: Recipe, model: ModelInfo): Promise<string> {
// if the recipe has an associate git
if(recipe.repository) {
const localFolder = path.join(this.appUserDirectory, recipe.id);

// clone the recipe repository on the local folder
Expand All @@ -131,26 +135,43 @@ export class ApplicationManager extends Publisher<ApplicationState[]> implements
'model-id': model.id,
});

// Get the absolute base folder
const source = recipe.basedir?path.join(localFolder, recipe.basedir):localFolder;
this.localRepositories.register({
path: gitCloneInfo.targetDirectory,
sourcePath: path.join(gitCloneInfo.targetDirectory, recipe.basedir ?? ''),
sourcePath: source,
labels: {
'recipe-id': recipe.id,
},
});

return source

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

View workflow job for this annotation

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

Missing semicolon

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

View workflow job for this annotation

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

Missing semicolon

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

View workflow job for this annotation

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

Missing semicolon
} else if(recipe.basedir && path.isAbsolute(recipe.basedir)) {
return recipe.basedir;
} else {
throw new Error('recipe is malformed: either a repository or an absolute basedir must be defined.');
}
}

async startApplication(recipe: Recipe, model: ModelInfo): Promise<void> {
// const recipeStatus = this.recipeStatusRegistry.
const startTime = performance.now();
try {
// Get the absolute path to the CONFIG_FILENAME folder
const localFolder = await this.getRecipeLocalFolder(recipe, model);

// load and parse the recipe configuration file and filter containers based on architecture, gpu accelerator
// and backend (that define which model supports)
const configAndFilteredContainers = this.getConfigAndFilterContainers(recipe.basedir, localFolder);
const configAndFilteredContainers = this.getConfigAndFilterContainers(localFolder);

// get model by downloading it or retrieving locally
let modelPath = await this.modelsManager.requestDownloadModel(model, {
await this.modelsManager.requestDownloadModel(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, {
const modelPath = await this.modelsManager.uploadModelToPodmanMachine(model, {
'recipe-id': recipe.id,
'model-id': model.id,
});
Expand Down Expand Up @@ -515,7 +536,6 @@ export class ApplicationManager extends Publisher<ApplicationState[]> implements
}

getConfigAndFilterContainers(
recipeBaseDir: string | undefined,
localFolder: string,
labels?: { [key: string]: string },
): AIContainers {
Expand All @@ -525,7 +545,7 @@ export class ApplicationManager extends Publisher<ApplicationState[]> implements
let aiConfigFile: AIConfigFile;
try {
// load and parse the recipe configuration file
aiConfigFile = this.getConfiguration(recipeBaseDir, localFolder);
aiConfigFile = this.getConfiguration(localFolder);
} catch (e) {
task.error = `Something went wrong while loading configuration: ${String(e)}.`;
this.taskRegistry.updateTask(task);
Expand Down Expand Up @@ -557,13 +577,8 @@ export class ApplicationManager extends Publisher<ApplicationState[]> implements
);
}

getConfiguration(recipeBaseDir: string | undefined, localFolder: string): AIConfigFile {
let configFile: string;
if (recipeBaseDir !== undefined) {
configFile = path.join(localFolder, recipeBaseDir, CONFIG_FILENAME);
} else {
configFile = path.join(localFolder, CONFIG_FILENAME);
}
getConfiguration(localFolder: string): AIConfigFile {
let configFile: string = path.join(localFolder, CONFIG_FILENAME);

if (!fs.existsSync(configFile)) {
throw new Error(`The file located at ${configFile} does not exist.`);
Expand Down
Loading

0 comments on commit da49abb

Please sign in to comment.