mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 15:46:43 +00:00
feat(stacks): per-service start/stop/restart lifecycle actions (#778)
* feat(stacks): add per-service start/stop/restart lifecycle routes
Adds POST /:stackName/services/:serviceName/{start,stop,restart} routes
that operate on containers belonging to a single Compose service, using
the same Engine API pattern as the existing stack-level lifecycle routes.
Includes isValidServiceName validator and audit-summary entries for the
three new paths.
* test(stacks): add per-service action route tests
* test(stacks): fix test quality issues in service action tests
* feat(stacks): add per-service lifecycle menu to container cards
* fix(stacks): handle paused container state in service action menu
* docs(stacks): add per-service lifecycle actions documentation
* docs(stacks): add validation screenshots for per-service lifecycle actions
This commit is contained in:
@@ -13,7 +13,7 @@ import { enforcePolicyPreDeploy } from '../services/PolicyEnforcement';
|
||||
import { requirePermission } from '../middleware/permissions';
|
||||
import { requirePaid, requireAdmin } from '../middleware/tierGates';
|
||||
import { NotificationService } from '../services/NotificationService';
|
||||
import { isValidStackName, isPathWithinBase } from '../utils/validation';
|
||||
import { isValidStackName, isValidServiceName, isPathWithinBase } from '../utils/validation';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { sendGitSourceError } from '../utils/gitSourceHttp';
|
||||
@@ -692,6 +692,65 @@ stacksRouter.post('/:stackName/start', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
type ServiceAction = 'start' | 'stop' | 'restart';
|
||||
|
||||
async function handleServiceAction(
|
||||
req: Request,
|
||||
res: Response,
|
||||
action: ServiceAction,
|
||||
): Promise<void> {
|
||||
const stackName = req.params.stackName as string;
|
||||
const serviceName = req.params.serviceName as string;
|
||||
if (!requirePermission(req, res, 'stack:deploy', 'stack', stackName)) return;
|
||||
if (!isValidStackName(stackName)) {
|
||||
res.status(400).json({ error: 'Invalid stack name' });
|
||||
return;
|
||||
}
|
||||
if (!isValidServiceName(serviceName)) {
|
||||
res.status(400).json({ error: 'Invalid service name' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const dockerController = DockerController.getInstance(req.nodeId);
|
||||
const all = await dockerController.getContainersByStack(stackName);
|
||||
if (!all || all.length === 0) {
|
||||
res.status(404).json({ error: 'No containers found for this stack.' });
|
||||
return;
|
||||
}
|
||||
const matching = all.filter(c => c.Service === serviceName);
|
||||
if (matching.length === 0) {
|
||||
res.status(404).json({ error: `Service '${serviceName}' not found in stack '${stackName}'.` });
|
||||
return;
|
||||
}
|
||||
const op =
|
||||
action === 'start'
|
||||
? (id: string) => dockerController.startContainer(id)
|
||||
: action === 'stop'
|
||||
? (id: string) => dockerController.stopContainer(id)
|
||||
: (id: string) => dockerController.restartContainer(id);
|
||||
await Promise.all(matching.map(c => op(c.Id)));
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
console.log(
|
||||
`[Stacks] Service ${action} completed: ${stackName}/${serviceName} (${matching.length} containers)`,
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Service ${action} completed via Engine API.`,
|
||||
count: matching.length,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error(`[Stacks] Service ${action} failed: ${stackName}/${serviceName}`, error);
|
||||
res.status(500).json({ error: getErrorMessage(error, `Failed to ${action} service`) });
|
||||
}
|
||||
}
|
||||
|
||||
stacksRouter.post('/:stackName/services/:serviceName/restart', (req, res) =>
|
||||
handleServiceAction(req, res, 'restart'));
|
||||
stacksRouter.post('/:stackName/services/:serviceName/stop', (req, res) =>
|
||||
handleServiceAction(req, res, 'stop'));
|
||||
stacksRouter.post('/:stackName/services/:serviceName/start', (req, res) =>
|
||||
handleServiceAction(req, res, 'start'));
|
||||
|
||||
stacksRouter.get('/:stackName/update-preview', async (req: Request, res: Response) => {
|
||||
const stackName = req.params.stackName as string;
|
||||
if (!isValidStackName(stackName)) {
|
||||
|
||||
Reference in New Issue
Block a user