Skip to content
Merged
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
9 changes: 9 additions & 0 deletions src/commands/general/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import simulatorService from "../../lib/services/simulator";
import { initAction, InitActionOptions } from "./init";
import { startAction, StartActionOptions } from "./start";
import {localnetCompatibleVersion} from "../../lib/config/simulator";
import {StopAction} from "./stop";

export function initializeGeneralCommands(program: Command) {
program
Expand All @@ -24,5 +25,13 @@ export function initializeGeneralCommands(program: Command) {
.option("--reset-db", "Reset Database", false)
.action((options: StartActionOptions) => startAction(options, simulatorService));

program
.command("stop")
.description("Stop all running localnet services.")
.action(async () => {
const stopAction = new StopAction();
await stopAction.stop();
});

return program;
}
25 changes: 25 additions & 0 deletions src/commands/general/stop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BaseAction } from "../../lib/actions/BaseAction";
import { SimulatorService } from "../../lib/services/simulator";
import { ISimulatorService } from "../../lib/interfaces/ISimulatorService";

export class StopAction extends BaseAction {
private simulatorService: ISimulatorService;

constructor() {
super();
this.simulatorService = new SimulatorService();
}

public async stop(): Promise<void> {
try{
await this.confirmPrompt(
"Are you sure you want to stop all running GenLayer containers? This will halt all active processes."
);
console.log(`Stopping Docker containers...`);
await this.simulatorService.stopDockerContainers();
console.log(`All running GenLayer containers have been successfully stopped.`);
}catch (error) {
console.error("An error occurred while stopping the containers:", error)
}
}
}
5 changes: 3 additions & 2 deletions src/lib/interfaces/ISimulatorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ export interface ISimulatorService {
getAiProvidersOptions(withHint: boolean): Array<{name: string; value: string}>;
getFrontendUrl(): string;
openFrontend(): Promise<boolean>;
resetDockerContainers(): Promise<boolean>;
resetDockerImages(): Promise<boolean>;
stopDockerContainers(): Promise<void>;
resetDockerContainers(): Promise<void>;
resetDockerImages(): Promise<void>;
checkCliVersion(): Promise<void>;
cleanDatabase(): Promise<boolean>;
addConfigToEnvFile(newConfig: Record<string, string>): void;
Expand Down
66 changes: 40 additions & 26 deletions src/lib/services/simulator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Docker from "dockerode"
import Docker, {ContainerInfo} from "dockerode";
import * as fs from "fs";
import * as dotenv from "dotenv";
import * as path from "path";
Expand Down Expand Up @@ -48,6 +48,39 @@ export class SimulatorService implements ISimulatorService {
this.docker = new Docker();
}

private readEnvConfigValue(key: string): string {
const envFilePath = path.join(this.location, ".env");
// Transform the config string to object
const envConfig = dotenv.parse(fs.readFileSync(envFilePath, "utf8"));
return envConfig[key];
}

private async getGenlayerContainers(): Promise<ContainerInfo[]> {
const containers = await this.docker.listContainers({ all: true });
return containers.filter(container =>
container.Names.some(name =>
name.startsWith(CONTAINERS_NAME_PREFIX) || name.includes("ollama")
)
);
}

private async stopAndRemoveContainers(remove: boolean = false): Promise<void> {
const genlayerContainers = await this.getGenlayerContainers();

for (const containerInfo of genlayerContainers) {
const container = this.docker.getContainer(containerInfo.Id);
if (containerInfo.State === "running") {
await container.stop();
}

const isOllamaContainer = containerInfo.Names.some(name => name.includes("ollama"));

if (remove && !isOllamaContainer) {
await container.remove();
}
}
}

public addConfigToEnvFile(newConfig: Record<string, string>): void {
const envFilePath = path.join(this.location, ".env");

Expand Down Expand Up @@ -76,13 +109,6 @@ export class SimulatorService implements ISimulatorService {
return this.composeOptions;
}

private readEnvConfigValue(key: string): string {
const envFilePath = path.join(this.location, ".env");
// Transform the config string to object
const envConfig = dotenv.parse(fs.readFileSync(envFilePath, "utf8"));
return envConfig[key];
}

public async checkCliVersion(): Promise<void> {
const update = await updateCheck(pkg);
if (update && update.latest !== pkg.version) {
Expand Down Expand Up @@ -218,25 +244,15 @@ export class SimulatorService implements ISimulatorService {
return true;
}

public async resetDockerContainers(): Promise<boolean> {
const containers = await this.docker.listContainers({ all: true });
const genlayerContainers = containers.filter(container =>
container.Names.some(name =>
name.startsWith(CONTAINERS_NAME_PREFIX)
)
);
public async stopDockerContainers(): Promise<void> {
await this.stopAndRemoveContainers(false);
}

for (const containerInfo of genlayerContainers) {
const container = this.docker.getContainer(containerInfo.Id);
if (containerInfo.State === "running") {
await container.stop();
}
await container.remove();
}
return true;
public async resetDockerContainers(): Promise<void> {
await this.stopAndRemoveContainers(true);
}

public async resetDockerImages(): Promise<boolean> {
public async resetDockerImages(): Promise<void> {
const images = await this.docker.listImages();
const genlayerImages = images.filter(image =>
image.RepoTags?.some(tag => tag.startsWith(IMAGES_NAME_PREFIX))
Expand All @@ -246,8 +262,6 @@ export class SimulatorService implements ISimulatorService {
const image = this.docker.getImage(imageInfo.Id);
await image.remove({force: true});
}

return true;
}

public async cleanDatabase(): Promise<boolean> {
Expand Down
58 changes: 58 additions & 0 deletions tests/actions/stop.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, test, vi, beforeEach, afterEach, expect } from "vitest";
import { StopAction } from "../../src/commands/general/stop";
import { SimulatorService } from "../../src/lib/services/simulator";
import { ISimulatorService } from "../../src/lib/interfaces/ISimulatorService";
import inquirer from "inquirer";

vi.mock("../../src/lib/services/simulator");
vi.mock("inquirer");

describe("StopAction", () => {
let stopAction: StopAction;
let mockSimulatorService: ISimulatorService;

beforeEach(() => {
vi.clearAllMocks();

mockSimulatorService = {
stopDockerContainers: vi.fn(),
} as unknown as ISimulatorService;

SimulatorService.prototype.stopDockerContainers = mockSimulatorService.stopDockerContainers;

stopAction = new StopAction();
(stopAction as any).simulatorService = mockSimulatorService;
});

afterEach(() => {
vi.restoreAllMocks();
});

test("should stop containers if user confirms", async () => {
vi.mocked(inquirer.prompt).mockResolvedValue({ confirmAction: true });

await stopAction.stop();

expect(inquirer.prompt).toHaveBeenCalledWith([
{
type: "confirm",
name: "confirmAction",
message: "Are you sure you want to stop all running GenLayer containers? This will halt all active processes.",
default: true,
},
]);
expect(mockSimulatorService.stopDockerContainers).toHaveBeenCalled();
});

test("should abort if user cancels", async () => {
vi.mocked(inquirer.prompt).mockResolvedValue({ confirmAction: false });

console.log = vi.fn();

await stopAction.stop();

expect(inquirer.prompt).toHaveBeenCalled();
expect(console.log).toHaveBeenCalledWith("Operation aborted!");
expect(mockSimulatorService.stopDockerContainers).not.toHaveBeenCalled();
});
});
27 changes: 27 additions & 0 deletions tests/commands/stop.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Command } from "commander";
import { vi, describe, beforeEach, afterEach, test, expect } from "vitest";
import { initializeGeneralCommands } from "../../src/commands/general";
import { StopAction } from "../../src/commands/general/stop";

vi.mock("../../src/commands/general/stop");

describe("stop command", () => {
let program: Command;

beforeEach(() => {
program = new Command();
initializeGeneralCommands(program);

vi.clearAllMocks();
});

afterEach(() => {
vi.restoreAllMocks();
});

test("doesn't require arguments or options", async () => {
expect(() => program.parse(["node", "test", "stop"])).not.toThrow();
expect(StopAction).toHaveBeenCalledTimes(1);
expect(StopAction.prototype.stop).toHaveBeenCalledWith();
});
});
34 changes: 32 additions & 2 deletions tests/services/simulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ describe("SimulatorService - Docker Tests", () => {

const result = await simulatorService.resetDockerContainers();

expect(result).toBe(true);
expect(result).toBe(undefined);
expect(mockListContainers).toHaveBeenCalledWith({ all: true });

// Ensure only the relevant containers were stopped and removed
Expand All @@ -341,6 +341,36 @@ describe("SimulatorService - Docker Tests", () => {
expect(mockRemove).toHaveBeenCalledTimes(2);
});

test("should stop all running GenLayer containers", async () => {
const mockContainers = [
{
Id: "container1",
Names: [`${CONTAINERS_NAME_PREFIX}container1`],
State: "running",
},
{
Id: "container2",
Names: [`${CONTAINERS_NAME_PREFIX}container2`],
State: "exited",
},
];

vi.mocked(Docker.prototype.listContainers).mockResolvedValue(mockContainers as any);

const mockStop = vi.fn().mockResolvedValue(undefined);
const mockGetContainer = vi.mocked(Docker.prototype.getContainer);
mockGetContainer.mockImplementation(() => ({
stop: mockStop,
} as unknown as Docker.Container));

await simulatorService.stopDockerContainers();

expect(mockGetContainer).toHaveBeenCalledWith("container1");
expect(mockGetContainer).toHaveBeenCalledWith("container2");
expect(mockStop).toHaveBeenCalledTimes(1);
});


test("should remove Docker images with the specified prefix", async () => {
const mockImages = [
{
Expand All @@ -366,7 +396,7 @@ describe("SimulatorService - Docker Tests", () => {

const result = await simulatorService.resetDockerImages();

expect(result).toBe(true);
expect(result).toBe(undefined);
expect(mockListImages).toHaveBeenCalled();
expect(mockGetImage).toHaveBeenCalledWith("image1");
expect(mockGetImage).toHaveBeenCalledWith("image2");
Expand Down
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
testTimeout: 10000,
coverage: {
exclude: [...configDefaults.exclude, '*.js', 'tests/**/*.ts', 'src/types', 'scripts'],
}
Expand Down
Loading