Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion apps/desktop/src/features/app-sidebar/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ function SidebarContent({ activeNavId, onNavSelect }: ContentProps) {
id: "docker",
label: "Docker Manager",
icon: Container,
disabled: true,
onClick: () => onNavSelect?.("docker"),
},
];

Expand Down
228 changes: 228 additions & 0 deletions apps/desktop/src/features/docker-manager/api/container-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import type {
PostgresContainerConfig,
DockerContainer,
CreateContainerResult,
ContainerActionResult,
RemoveContainerOptions,
} from "../types";
import {
POSTGRES_IMAGE,
POSTGRES_CONTAINER_PORT,
MANAGED_LABEL_KEY,
MANAGED_LABEL_VALUE,
} from "../constants";
import { validateContainerName, generateVolumeName } from "../utilities/container-naming";
import {
checkDockerAvailability,
listContainers,
getContainerDetails,
startContainer as clientStartContainer,
stopContainer as clientStopContainer,
restartContainer as clientRestartContainer,
removeContainer as clientRemoveContainer,
pullImage,
imageExists,
} from "./docker-client";

async function executeDockerCommand(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
if (typeof window !== "undefined" && "Tauri" in window) {
const { Command } = await import("@tauri-apps/plugin-shell");
const command = Command.create("docker", args);
const output = await command.execute();
return {
stdout: output.stdout,
stderr: output.stderr,
exitCode: output.code ?? 0,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Avoid duplicating executeDockerCommand implementation between client modules.

This implementation duplicates the version in docker-client.ts, which risks behavior drift (env, error handling, logging). Prefer reusing that helper directly or extracting it to a shared module so all Docker callers stay aligned.

}

throw new Error("Docker commands require Tauri shell plugin");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Exit code fallback to 0 may mask command failures.

When output.code is null, the fallback to 0 treats it as success. A null exit code typically indicates the command didn't complete normally. Consider treating it as an error.

🐛 Safer exit code handling
         return {
             stdout: output.stdout,
             stderr: output.stderr,
-            exitCode: output.code ?? 0,
+            exitCode: output.code ?? -1, // Treat null as failure
         };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function executeDockerCommand(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
if (typeof window !== "undefined" && "Tauri" in window) {
const { Command } = await import("@tauri-apps/plugin-shell");
const command = Command.create("docker", args);
const output = await command.execute();
return {
stdout: output.stdout,
stderr: output.stderr,
exitCode: output.code ?? 0,
};
}
throw new Error("Docker commands require Tauri shell plugin");
}
async function executeDockerCommand(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
if (typeof window !== "undefined" && "Tauri" in window) {
const { Command } = await import("@tauri-apps/plugin-shell");
const command = Command.create("docker", args);
const output = await command.execute();
return {
stdout: output.stdout,
stderr: output.stderr,
exitCode: output.code ?? -1, // Treat null as failure
};
}
throw new Error("Docker commands require Tauri shell plugin");
}
🤖 Prompt for AI Agents
In `@apps/desktop/src/features/docker-manager/api/container-service.ts` around
lines 27 - 40, The current executeDockerCommand function treats a null
output.code as success by falling back to 0; change this so a null exit code is
treated as failure: inside executeDockerCommand, detect when output.code is null
(output.code === null || output.code === undefined) and either throw a
descriptive Error that includes the command args, output.stdout and
output.stderr, or set exitCode to a non-zero sentinel (e.g., -1) and mark the
call as failed; update the returned object or thrown error accordingly so
callers of executeDockerCommand can reliably detect a non-normal termination.


export async function createPostgresContainer(
config: PostgresContainerConfig
): Promise<CreateContainerResult> {
const validation = validateContainerName(config.name);
if (!validation.valid) {
return { success: false, error: validation.error };
}

const availability = await checkDockerAvailability();
if (!availability.available) {
return { success: false, error: availability.error };
}

const imageTag = config.postgresVersion || "16";
const hasImage = await imageExists(POSTGRES_IMAGE, imageTag);

if (!hasImage) {
try {
await pullImage(POSTGRES_IMAGE, imageTag);
} catch (error) {
return {
success: false,
error: `Failed to pull PostgreSQL image: ${error instanceof Error ? error.message : String(error)}`
};
}
}

const args = buildCreateContainerArgs(config, imageTag);

try {
const result = await executeDockerCommand(args);

if (result.exitCode !== 0) {
return { success: false, error: result.stderr || "Failed to create container" };
}

const containerId = result.stdout.trim();

const startResult = await executeDockerCommand(["start", containerId]);
if (startResult.exitCode !== 0) {
return {
success: false,
error: startResult.stderr || "Container created but failed to start"
};
}

return { success: true, containerId };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error creating container"
};
}
}

function buildCreateContainerArgs(config: PostgresContainerConfig, imageTag: string): string[] {
const args = [
"create",
"--name", config.name,
"--label", `${MANAGED_LABEL_KEY}=${MANAGED_LABEL_VALUE}`,
"-e", `POSTGRES_USER=${config.user}`,
"-e", `POSTGRES_PASSWORD=${config.password}`,
"-e", `POSTGRES_DB=${config.database}`,
"-p", `${config.hostPort}:${POSTGRES_CONTAINER_PORT}`,
"--health-cmd", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}",
"--health-interval", "5s",
"--health-timeout", "5s",
"--health-retries", "5",
"--health-start-period", "10s",
];

if (!config.ephemeral) {
const volumeName = config.volumeName || generateVolumeName(config.name);
args.push("-v", `${volumeName}:/var/lib/postgresql/data`);
}

if (config.cpuLimit) {
args.push("--cpus", String(config.cpuLimit));
}

if (config.memoryLimitMb) {
args.push("-m", `${config.memoryLimitMb}m`);
}

args.push(`${POSTGRES_IMAGE}:${imageTag}`);

return args;
}

export async function performContainerAction(
containerId: string,
action: "start" | "stop" | "restart"
): Promise<ContainerActionResult> {
try {
switch (action) {
case "start":
await clientStartContainer(containerId);
break;
case "stop":
await clientStopContainer(containerId);
break;
case "restart":
await clientRestartContainer(containerId);
break;
}
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : `Failed to ${action} container`
};
}
}

export async function deleteContainer(
containerId: string,
options: RemoveContainerOptions = { removeVolumes: false, force: true }
): Promise<ContainerActionResult> {
try {
await clientRemoveContainer(containerId, {
force: options.force,
removeVolumes: options.removeVolumes,
});
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Failed to remove container"
};
}
}

export async function getContainers(
showAll: boolean = true,
showExternal: boolean = false
): Promise<DockerContainer[]> {
const containers = await listContainers(showAll, !showExternal);

return containers.sort(function (a, b) {
if (a.origin === "managed" && b.origin !== "managed") return -1;
if (a.origin !== "managed" && b.origin === "managed") return 1;
return b.createdAt - a.createdAt;
});
}

export async function getContainer(containerId: string): Promise<DockerContainer | null> {
return getContainerDetails(containerId);
}

export async function waitForHealthy(
containerId: string,
timeoutMs: number = 30000,
intervalMs: number = 1000
): Promise<boolean> {
const startTime = Date.now();

while (Date.now() - startTime < timeoutMs) {
const container = await getContainerDetails(containerId);

if (container?.health === "healthy") {
return true;
}

if (container?.health === "unhealthy") {
return false;
}

if (container?.state !== "running") {
return false;
}

await sleep(intervalMs);
}

return false;
}

function sleep(ms: number): Promise<void> {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}

export {
checkDockerAvailability,
getContainerLogs
} from "./docker-client";
Loading
Loading