mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
feat: update stack deployment endpoint and enhance deployment functionality
This commit is contained in:
+12
-38
@@ -478,13 +478,14 @@ app.post('/api/containers/:id/restart', async (req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
// End of legacy container routes
|
||||
app.post('/api/stacks/:stackName/up', async (req: Request, res: Response) => {
|
||||
app.post('/api/stacks/:stackName/deploy', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const stackName = req.params.stackName as string;
|
||||
composeService.runCommand(stackName, 'up', terminalWs || undefined);
|
||||
res.json({ status: 'Command started' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to start command' });
|
||||
await composeService.deployStack(stackName, terminalWs || undefined);
|
||||
res.json({ message: 'Deployed successfully' });
|
||||
} catch (error: any) {
|
||||
console.error('Failed to deploy stack:', error);
|
||||
res.status(500).json({ error: error.message || 'Failed to deploy stack' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -498,60 +499,33 @@ app.post('/api/stacks/:stackName/down', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Direct container restart - bypasses docker compose for legacy container support
|
||||
app.post('/api/stacks/:stackName/restart', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const stackName = req.params.stackName as string;
|
||||
const dockerController = DockerController.getInstance();
|
||||
const containers = await dockerController.getContainersByStack(stackName);
|
||||
if (containers && containers.length > 0) {
|
||||
for (const c of containers) {
|
||||
await dockerController.restartContainer(c.Id);
|
||||
}
|
||||
}
|
||||
res.json({ status: 'Containers restarted' });
|
||||
composeService.runCommand(stackName, 'restart', terminalWs || undefined);
|
||||
res.json({ status: 'Command started' });
|
||||
} catch (error) {
|
||||
console.error('Failed to restart containers:', error);
|
||||
res.status(500).json({ error: 'Failed to restart containers' });
|
||||
}
|
||||
});
|
||||
|
||||
// Direct container stop - bypasses docker compose for legacy container support
|
||||
// Only stops containers that are currently running to avoid 304 errors
|
||||
app.post('/api/stacks/:stackName/stop', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const stackName = req.params.stackName as string;
|
||||
const dockerController = DockerController.getInstance();
|
||||
const containers = await dockerController.getContainersByStack(stackName);
|
||||
if (containers && containers.length > 0) {
|
||||
for (const c of containers) {
|
||||
if (c.State === 'running') {
|
||||
await dockerController.stopContainer(c.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.json({ status: 'Containers stopped' });
|
||||
composeService.runCommand(stackName, 'stop', terminalWs || undefined);
|
||||
res.json({ status: 'Command started' });
|
||||
} catch (error) {
|
||||
console.error('Failed to stop containers:', error);
|
||||
res.status(500).json({ error: 'Failed to stop containers' });
|
||||
}
|
||||
});
|
||||
|
||||
// Direct container start - bypasses docker compose for legacy container support
|
||||
// Only starts containers that are not currently running
|
||||
app.post('/api/stacks/:stackName/start', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const stackName = req.params.stackName as string;
|
||||
const dockerController = DockerController.getInstance();
|
||||
const containers = await dockerController.getContainersByStack(stackName);
|
||||
if (containers && containers.length > 0) {
|
||||
for (const c of containers) {
|
||||
if (c.State !== 'running') {
|
||||
await dockerController.startContainer(c.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.json({ status: 'Containers started' });
|
||||
composeService.runCommand(stackName, 'start', terminalWs || undefined);
|
||||
res.json({ status: 'Command started' });
|
||||
} catch (error) {
|
||||
console.error('Failed to start containers:', error);
|
||||
res.status(500).json({ error: 'Failed to start containers' });
|
||||
|
||||
@@ -16,14 +16,12 @@ export class ComposeService {
|
||||
* CRITICAL: cwd is set to the stack directory so relative paths in compose files
|
||||
* resolve correctly inside the isolated stack folder
|
||||
*/
|
||||
runCommand(stackName: string, action: 'up' | 'down', ws?: WebSocket) {
|
||||
runCommand(stackName: string, action: 'down' | 'start' | 'stop' | 'restart', ws?: WebSocket) {
|
||||
const stackDir = path.join(this.baseDir, stackName);
|
||||
|
||||
// Run docker compose from within the stack directory
|
||||
// This ensures relative paths (e.g., ./data:/config) resolve correctly
|
||||
const args = action === 'up'
|
||||
? ['compose', 'up', '-d']
|
||||
: ['compose', 'down'];
|
||||
const args = ['compose', '-p', stackName, action];
|
||||
|
||||
const child = spawn('docker', args, {
|
||||
cwd: stackDir, // CRITICAL: Set working directory to stack folder
|
||||
@@ -53,6 +51,54 @@ export class ComposeService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy stack: executes up -d --remove-orphans and awaits completion.
|
||||
*/
|
||||
async deployStack(stackName: string, ws?: WebSocket): Promise<void> {
|
||||
const stackDir = path.join(this.baseDir, stackName);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = ['compose', '-p', stackName, 'up', '-d', '--remove-orphans'];
|
||||
const child = spawn('docker', args, {
|
||||
cwd: stackDir,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
|
||||
}
|
||||
});
|
||||
|
||||
let errorLog = '';
|
||||
|
||||
if (ws) {
|
||||
child.stdout.on('data', (data: Buffer) => ws.send(data.toString()));
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
errorLog += text;
|
||||
ws.send(text);
|
||||
});
|
||||
child.on('close', (code: number | null) => {
|
||||
ws.send(`Command exited with code ${code}\n`);
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(errorLog.trim() || `Command failed with code ${code}`));
|
||||
});
|
||||
} else {
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
errorLog += data.toString();
|
||||
});
|
||||
child.on('close', (code: number | null) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(errorLog.trim() || `Command failed with code ${code}`));
|
||||
});
|
||||
}
|
||||
|
||||
child.on('error', (error: Error) => {
|
||||
console.error(`Docker Compose Deploy Error for ${stackName}:`, error.message);
|
||||
if (ws) ws.send(`Error: ${error.message}\n`);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream docker logs for a stack via WebSocket.
|
||||
* Supervisors: Fetches containers via DockerController and spawns `docker logs -f` for each.
|
||||
@@ -199,7 +245,7 @@ export class ComposeService {
|
||||
// Step 1: Pull images
|
||||
sendOutput('=== Pulling latest images ===\n');
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const pullProcess = spawn('docker', ['compose', 'pull'], {
|
||||
const pullProcess = spawn('docker', ['compose', '-p', stackName, 'pull'], {
|
||||
cwd: stackDir,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -235,7 +281,7 @@ export class ComposeService {
|
||||
// Step 2: Recreate containers with new images
|
||||
sendOutput('=== Recreating containers ===\n');
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const upProcess = spawn('docker', ['compose', 'up', '-d'], {
|
||||
const upProcess = spawn('docker', ['compose', '-p', stackName, 'up', '-d', '--remove-orphans'], {
|
||||
cwd: stackDir,
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
||||
import { Tabs, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Plus, Trash2, Play, Square, Save, Terminal, Sun, Moon, RotateCw, CloudDownload, Pencil, X, Home, LogOut, Brush, ExternalLink, Bell, Settings, MoreVertical, BellRing } from 'lucide-react';
|
||||
import { Plus, Trash2, Play, Square, Save, Terminal, Sun, Moon, RotateCw, CloudDownload, Pencil, X, Home, LogOut, Brush, ExternalLink, Bell, Settings, MoreVertical, BellRing, Rocket } from 'lucide-react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { toast } from 'sonner';
|
||||
@@ -346,16 +346,18 @@ export default function EditorLayout() {
|
||||
const stackName = selectedFile.replace(/\.(yml|yaml)$/, '');
|
||||
setIsActionLoading(true);
|
||||
try {
|
||||
await apiFetch(`/stacks/${stackName}/up`, {
|
||||
await apiFetch(`/stacks/${stackName}/deploy`, {
|
||||
method: 'POST',
|
||||
});
|
||||
toast.success("Stack deployed successfully!");
|
||||
// Refresh containers after deploy
|
||||
const containersRes = await apiFetch(`/stacks/${stackName}/containers`);
|
||||
const conts = await containersRes.json();
|
||||
setContainers(Array.isArray(conts) ? conts : []);
|
||||
await refreshStacks(true);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Failed to deploy:', error);
|
||||
toast.error(error.message || "Failed to deploy stack");
|
||||
} finally {
|
||||
setIsActionLoading(false);
|
||||
}
|
||||
@@ -831,12 +833,10 @@ export default function EditorLayout() {
|
||||
<CardTitle className="text-2xl font-bold">{stackName}</CardTitle>
|
||||
{/* Action Bar */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{!isDeployed && (
|
||||
<Button type="button" size="sm" className="rounded-lg" onClick={deployStack} disabled={isActionLoading}>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{isActionLoading ? 'Working...' : 'Deploy'}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" size="sm" variant="default" className="rounded-lg" onClick={deployStack} disabled={isActionLoading}>
|
||||
<Rocket className="w-4 h-4 mr-2" />
|
||||
{isActionLoading ? 'Deploying...' : 'Deploy'}
|
||||
</Button>
|
||||
{isDeployed && (
|
||||
<>
|
||||
{isRunning ? (
|
||||
@@ -845,7 +845,7 @@ export default function EditorLayout() {
|
||||
{isActionLoading ? 'Working...' : 'Stop'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="button" size="sm" className="rounded-lg" onClick={startStack} disabled={isActionLoading}>
|
||||
<Button type="button" size="sm" variant="outline" className="rounded-lg" onClick={startStack} disabled={isActionLoading}>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{isActionLoading ? 'Working...' : 'Start'}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user