Skip to content

Commit

Permalink
Restart environment (#275)
Browse files Browse the repository at this point in the history
* restart environment

* change animation when deleting/restarting

* fix lint

* fix unit tests
  • Loading branch information
feloy authored Feb 13, 2024
1 parent 0cf70c0 commit 8e06f4c
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 16 deletions.
31 changes: 26 additions & 5 deletions packages/backend/src/managers/applicationManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,9 @@ describe('createPod', async () => {
);
test('throw an error if there is no sample image', async () => {
const images = [imageInfo2];
await expect(manager.createPod({ id: 'recipe-id' } as Recipe, images)).rejects.toThrowError('no sample app found');
await expect(
manager.createPod({ id: 'recipe-id' } as Recipe, { id: 'model-id' } as ModelInfo, images),
).rejects.toThrowError('no sample app found');
});
test('call createPod with sample app exposed port', async () => {
const images = [imageInfo1, imageInfo2];
Expand All @@ -709,7 +711,7 @@ describe('createPod', async () => {
Id: 'podId',
engineId: 'engineId',
});
await manager.createPod({ id: 'recipe-id' } as Recipe, images);
await manager.createPod({ id: 'recipe-id' } as Recipe, { id: 'model-id' } as ModelInfo, images);
expect(mocks.createPodMock).toBeCalledWith({
name: 'name',
portmappings: [
Expand All @@ -730,6 +732,7 @@ describe('createPod', async () => {
],
labels: {
'ai-studio-recipe-id': 'recipe-id',
'ai-studio-model-id': 'model-id',
},
});
});
Expand Down Expand Up @@ -759,7 +762,13 @@ describe('createApplicationPod', () => {
test('throw if createPod fails', async () => {
vi.spyOn(manager, 'createPod').mockRejectedValue('error createPod');
await expect(
manager.createApplicationPod({ id: 'recipe-id' } as Recipe, images, 'path', taskUtils),
manager.createApplicationPod(
{ id: 'recipe-id' } as Recipe,
{ id: 'model-id' } as ModelInfo,
images,
'path',
taskUtils,
),
).rejects.toThrowError('error createPod');
expect(setTaskMock).toBeCalledWith({
error: 'Something went wrong while creating pod: error createPod',
Expand All @@ -778,7 +787,13 @@ describe('createApplicationPod', () => {
const createAndAddContainersToPodMock = vi
.spyOn(manager, 'createAndAddContainersToPod')
.mockImplementation((_pod: PodInfo, _images: ImageInfo[], _modelPath: string) => Promise.resolve([]));
await manager.createApplicationPod({ id: 'recipe-id' } as Recipe, images, 'path', taskUtils);
await manager.createApplicationPod(
{ id: 'recipe-id' } as Recipe,
{ id: 'model-id' } as ModelInfo,
images,
'path',
taskUtils,
);
expect(createAndAddContainersToPodMock).toBeCalledWith(pod, images, 'path');
expect(setTaskMock).toBeCalledWith({
id: 'id',
Expand All @@ -795,7 +810,13 @@ describe('createApplicationPod', () => {
vi.spyOn(manager, 'createPod').mockResolvedValue(pod);
vi.spyOn(manager, 'createAndAddContainersToPod').mockRejectedValue('error');
await expect(() =>
manager.createApplicationPod({ id: 'recipe-id' } as Recipe, images, 'path', taskUtils),
manager.createApplicationPod(
{ id: 'recipe-id' } as Recipe,
{ id: 'model-id' } as ModelInfo,
images,
'path',
taskUtils,
),
).rejects.toThrowError('error');
expect(setTaskMock).toHaveBeenLastCalledWith({
id: 'id',
Expand Down
9 changes: 6 additions & 3 deletions packages/backend/src/managers/applicationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type { ModelsManager } from './modelsManager';
import { getPortsInfo } from '../utils/ports';
import { goarch } from '../utils/arch';
import { getDurationSecondsSince, isEndpointAlive, timeout } from '../utils/utils';
import { LABEL_MODEL_ID } from './playground';

export const LABEL_RECIPE_ID = 'ai-studio-recipe-id';

Expand Down Expand Up @@ -102,7 +103,7 @@ export class ApplicationManager {
);

// create a pod containing all the containers to run the application
const podInfo = await this.createApplicationPod(recipe, images, modelPath, taskUtil);
const podInfo = await this.createApplicationPod(recipe, model, images, modelPath, taskUtil);

await this.runApplication(podInfo, taskUtil);
taskUtil.setStatus('running');
Expand Down Expand Up @@ -183,14 +184,15 @@ export class ApplicationManager {

async createApplicationPod(
recipe: Recipe,
model: ModelInfo,
images: ImageInfo[],
modelPath: string,
taskUtil: RecipeStatusUtils,
): Promise<PodInfo> {
// create empty pod
let podInfo: PodInfo;
try {
podInfo = await this.createPod(recipe, images);
podInfo = await this.createPod(recipe, model, images);
} catch (e) {
console.error('error when creating pod', e);
taskUtil.setTask({
Expand Down Expand Up @@ -301,7 +303,7 @@ export class ApplicationManager {
return containers;
}

async createPod(recipe: Recipe, images: ImageInfo[]): Promise<PodInfo> {
async createPod(recipe: Recipe, model: ModelInfo, images: ImageInfo[]): Promise<PodInfo> {
// find the exposed port of the sample app so we can open its ports on the new pod
const sampleAppImageInfo = images.find(image => !image.modelService);
if (!sampleAppImageInfo) {
Expand Down Expand Up @@ -331,6 +333,7 @@ export class ApplicationManager {
portmappings: portmappings,
labels: {
[LABEL_RECIPE_ID]: recipe.id,
[LABEL_MODEL_ID]: model.id,
},
});
return {
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/managers/environmentManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import type {
podStopHandle,
startupHandle,
} from './podmanConnection';
import type { ApplicationManager } from './applicationManager';
import type { CatalogManager } from './catalogManager';

let manager: EnvironmentManager;

Expand Down Expand Up @@ -82,6 +84,8 @@ beforeEach(() => {
startupSubscribe: mocks.startupSubscribe,
onMachineStop: mocks.onMachineStop,
} as unknown as PodmanConnection,
{} as ApplicationManager,
{} as CatalogManager,
);
});

Expand Down
18 changes: 18 additions & 0 deletions packages/backend/src/managers/environmentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@

import { type PodInfo, type Webview, containerEngine } from '@podman-desktop/api';
import type { PodmanConnection } from './podmanConnection';
import type { ApplicationManager } from './applicationManager';
import { LABEL_RECIPE_ID } from './applicationManager';
import { MSG_ENVIRONMENTS_STATE_UPDATE } from '@shared/Messages';
import type { EnvironmentState, EnvironmentStatus } from '@shared/src/models/IEnvironmentState';
import type { CatalogManager } from './catalogManager';
import { LABEL_MODEL_ID } from './playground';

/**
* An Environment is represented as a Pod, independently on how it has been created (by applicationManager or any other manager)
Expand All @@ -32,6 +35,8 @@ export class EnvironmentManager {
constructor(
private webview: Webview,
private podmanConnection: PodmanConnection,
private applicationManager: ApplicationManager,
private catalogManager: CatalogManager,
) {
this.#environments = new Map();
}
Expand Down Expand Up @@ -157,6 +162,19 @@ export class EnvironmentManager {
}
}

async restartEnvironment(recipeId: string) {
const envPod = await this.getEnvironmentPod(recipeId);
await this.deleteEnvironment(recipeId);
try {
const recipe = this.catalogManager.getRecipeById(recipeId);
const model = this.catalogManager.getModelById(envPod.Labels[LABEL_MODEL_ID]);
await this.applicationManager.pullApplication(recipe, model);
} catch (err: unknown) {
this.setEnvironmentStatus(recipeId, 'unknown');
throw err;
}
}

async getEnvironmentPod(recipeId: string): Promise<PodInfo> {
if (!containerEngine.listPods || !containerEngine.stopPod || !containerEngine.removePod) {
// TODO(feloy) this check can be safely removed when podman desktop 1.8 is released
Expand Down
26 changes: 26 additions & 0 deletions packages/backend/src/studio-api-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,32 @@ export class StudioApiImpl implements StudioAPI {
});
}

async requestRestartEnvironment(recipeId: string): Promise<void> {
const recipe = this.catalogManager.getRecipeById(recipeId);
// Do not wait on the promise as the api would probably timeout before the user answer.
podmanDesktopApi.window
.showWarningMessage(
`Restart the environment "${recipe.name}"? This will delete the containers running the application and model, rebuild the images with the current sources, and restart the containers.`,
'Confirm',
'Cancel',
)
.then((result: string) => {
if (result === 'Confirm') {
this.environmentManager.restartEnvironment(recipeId).catch((err: unknown) => {
console.error(`error restarting environment: ${String(err)}`);
podmanDesktopApi.window
.showErrorMessage(`Error restarting the environment "${recipe.name}"`)
.catch((err: unknown) => {
console.error(`Something went wrong with confirmation modals`, err);
});
});
}
})
.catch((err: unknown) => {
console.error(`Something went wrong with confirmation modals`, err);
});
}

async telemetryLogUsage(
eventName: string,
data?: Record<string, unknown | podmanDesktopApi.TelemetryTrustedValue>,
Expand Down
7 changes: 6 additions & 1 deletion packages/backend/src/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,12 @@ export class Studio {
this.modelsManager,
this.telemetry,
);
const envManager = new EnvironmentManager(this.#panel.webview, podmanConnection);
const envManager = new EnvironmentManager(
this.#panel.webview,
podmanConnection,
applicationManager,
this.catalogManager,
);

this.#panel.onDidChangeViewState((e: WebviewPanelOnDidChangeViewStateEvent) => {
this.telemetry.logUsage(e.webviewPanel.visible ? 'opened' : 'closed');
Expand Down
31 changes: 24 additions & 7 deletions packages/frontend/src/lib/table/environment/ColumnActions.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<script lang="ts">
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { faRotateForward, faTrash } from "@fortawesome/free-solid-svg-icons";
import ListItemButtonIcon from "../../button/ListItemButtonIcon.svelte";
import { studioClient } from "/@/utils/client";
import type { EnvironmentState } from "@shared/src/models/IEnvironmentState";
import Spinner from "../../button/Spinner.svelte";
export let object: EnvironmentState;
function deleteEnvironment() {
Expand All @@ -11,11 +12,27 @@ function deleteEnvironment() {
});
}
function restartEnvironment() {
studioClient.requestRestartEnvironment(object.recipeId).catch((err) => {
console.error(`Something went wrong while trying to restart environment: ${String(err)}.`);
});
}
</script>

<ListItemButtonIcon
icon={faTrash}
onClick={() => deleteEnvironment()}
title="Delete Environment"
inProgress={object.status === 'stopping' || object.status === 'removing'}
/>

{#if object.status === 'stopping' || object.status === 'removing'}
<div class="pr-4"><Spinner size="1.4em" /></div>
{:else}
<ListItemButtonIcon
icon={faTrash}
onClick={() => deleteEnvironment()}
title="Delete Environment"
/>

<ListItemButtonIcon
icon={faRotateForward}
onClick={() => restartEnvironment()}
title="Restart Environment"
/>
{/if}

1 change: 1 addition & 0 deletions packages/shared/src/StudioAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export abstract class StudioAPI {
abstract navigateToContainer(containerId: string): Promise<void>;
abstract getEnvironmentsState(): Promise<EnvironmentState[]>;
abstract requestRemoveEnvironment(recipeId: string): Promise<void>;
abstract requestRestartEnvironment(recipeId: string): Promise<void>;

abstract telemetryLogUsage(eventName: string, data?: Record<string, unknown | TelemetryTrustedValue>): Promise<void>;
abstract telemetryLogError(eventName: string, data?: Record<string, unknown | TelemetryTrustedValue>): Promise<void>;
Expand Down

0 comments on commit 8e06f4c

Please sign in to comment.