mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-18 22:36:19 +00:00
feat(scheduler): schedule container restart, stop, and start (#1526)
* feat(scheduler): schedule container restart, stop, and start Add container as a scheduled-task target type so operators can automate lifecycle actions against standalone containers by node and name, with matching UI pickers, validation, execution on local and remote nodes, and tests. * fix(scheduler): stack service matching and container picker hygiene Backfill Service on smartFallback containers so per-service stack restarts work when container_name is set. Match services by compose label and container name in stack routes and scheduled restarts. Exclude Sencho from GET /api/containers lists. Hide the Restart Stack service picker when a stack has only one service. * test(scheduler): scope service checkbox assertion to Services block The create dialog also has a Delete after run checkbox. Count checkboxes only inside the Services section so CI does not include unrelated form controls. * fix(scheduler): narrow closest() result to HTMLElement in schedule test The service-checkbox assertion passed an Element from closest() into within(), which requires an HTMLElement, failing tsc -b in the frontend build and Docker build stages. Use the closest<HTMLElement>() type argument so the value type-checks without an unsafe cast. * fix(scheduler): hide Sencho container on remote node picker lists Remote container lists are proxied from peer Sencho instances, so id-only self filtering missed peers on older builds. Await SelfIdentity init, match ImageID, and drop official saelix/sencho images. Apply the same heuristic in the scheduled-operations UI and when the hub fetches remote containers for scheduled runs. * test(monitor): add missing DatabaseService mocks for scan history cleanup * test(scheduler): add missing markStaleScansAsFailed mock SchedulerService.tick() calls db.markStaleScansAsFailed() to sweep stale vulnerability scans. The scheduler-service test was missing this method in its DatabaseService mock, causing TypeError failures during test initialization. Added mockMarkStaleScansAsFailed to hoisted mocks and DatabaseService mock object, returning safe default of 0 scans marked as failed. * test(compose): add missing FileSystemService mocks for getStackContent/getEnvContent * test(containers-route): mock SelfIdentityService to prevent initialize() crash The excludeSelfContainers() helper calls SelfIdentityService.initialize(), which tries to access DockerController. Without a proper SelfIdentityService mock, the initialize() call fails silently, causing a 500 error on GET /api/containers. Added SelfIdentityService mock with initialize(), isOwnContainer(), and isOwnImage() methods to prevent the crash.
This commit is contained in:
@@ -863,6 +863,31 @@ class DockerController {
|
||||
return this.validateApiData<any[]>(containers);
|
||||
}
|
||||
|
||||
/** Resolve a container by its durable name (not ephemeral ID). */
|
||||
public async findContainerByName(name: string): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
state: string;
|
||||
image: string;
|
||||
stackProject: string | null;
|
||||
} | null> {
|
||||
const normalized = name.replace(/^\//, '');
|
||||
const containers = await this.getAllContainers();
|
||||
for (const c of containers) {
|
||||
const containerName = c.Names?.[0]?.replace(/^\//, '');
|
||||
if (containerName === normalized) {
|
||||
return {
|
||||
id: c.Id,
|
||||
name: containerName,
|
||||
state: c.State ?? 'unknown',
|
||||
image: c.Image ?? '',
|
||||
stackProject: c.Labels?.['com.docker.compose.project'] ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds topology data with 2 Docker API calls instead of N+1.
|
||||
* Fetches all networks + all containers in parallel, then maps
|
||||
@@ -1488,10 +1513,13 @@ class DockerController {
|
||||
|
||||
// 2. Extract expected container names with legacy prefix support
|
||||
const expectedNames: string[] = [];
|
||||
const nameToService = new Map<string, string>();
|
||||
for (const [serviceName, serviceConfig] of Object.entries(parsedYaml.services)) {
|
||||
const config = serviceConfig as any;
|
||||
const config = serviceConfig as { container_name?: string };
|
||||
nameToService.set(serviceName, serviceName);
|
||||
if (config.container_name) {
|
||||
expectedNames.push(config.container_name);
|
||||
nameToService.set(config.container_name, serviceName);
|
||||
} else {
|
||||
// Standard v2 naming
|
||||
expectedNames.push(serviceName);
|
||||
@@ -1516,6 +1544,11 @@ class DockerController {
|
||||
|
||||
// 5. Map to the frontend interface
|
||||
return fallbackContainers.map(c => {
|
||||
const strippedName = c.Names?.[0]?.replace(/^\//, '') ?? '';
|
||||
const labelService = c.Labels?.['com.docker.compose.service'];
|
||||
const service = (typeof labelService === 'string' && labelService.length > 0
|
||||
? labelService
|
||||
: nameToService.get(strippedName)) ?? '';
|
||||
let Ports: { PrivatePort: number, PublicPort: number, Type?: string }[] = [];
|
||||
if (c.Ports && Array.isArray(c.Ports)) {
|
||||
Ports = c.Ports
|
||||
@@ -1525,8 +1558,10 @@ class DockerController {
|
||||
return {
|
||||
Id: c.Id,
|
||||
Names: c.Names,
|
||||
Service: service,
|
||||
State: c.State,
|
||||
Status: c.Status,
|
||||
Labels: c.Labels,
|
||||
Ports
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user