fix: harden docker api validation, handle sftp errors, and fix node manager ui

This commit is contained in:
SaelixCode
2026-03-18 14:13:07 -04:00
parent 69e86a0a37
commit 4bd80e29bf
5 changed files with 43 additions and 13 deletions
+9
View File
@@ -37,6 +37,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Changed:** Standardized manual stack deletion to use the Two-Stage Teardown (Compose Down -> File Wipe) to prevent ghost networks.
- **Fixed:** Atomic Rollback failure where non-empty directories caused silent file system errors.
- **Added:** Two-Stage Teardown mechanism to ensure `docker compose down` sweeps up ghost networks before deployment files are deleted.
### Deprecated
- **(Planned)** Port 2375 (TCP) fallback support; future releases may require SSH-only for Node config.
### Fixed
- **Fixed:** Docker API parsing bug where HTML string responses from misconfigured ports were counted as containers.
- **Fixed:** Stack list crashing when SFTP connections fail by gracefully catching SSH errors and returning empty arrays.
### Changed
- **Changed:** Expanded Node Manager UI width and added horizontal scrollbars for better data visibility.
- **Added:** Smart Error Parser with telemetry-ready rule IDs to translate cryptic Docker output.
- **Added:** Post-Deploy Health Probe to catch immediate container crashes that slip past Compose.
- **Changed:** Rollback engine respects a `canSilentlyRollback` flag to protect user-authored configurations.
+16 -5
View File
@@ -29,6 +29,14 @@ class DockerController {
return this.docker;
}
private validateApiData<T>(data: any): T {
// If the daemon port points to a web server (like Sencho UI), Dockerode receives HTML
if (typeof data === 'string') {
throw new Error("Invalid response from Docker API. Did you provide a web port instead of the Docker daemon port?");
}
return data as T;
}
public async getDiskUsage() {
const df = await this.docker.df();
@@ -89,16 +97,19 @@ class DockerController {
}
public async getImages() {
return await this.docker.listImages({ all: false });
const data = await this.docker.listImages({ all: false });
return this.validateApiData<any[]>(data);
}
public async getVolumes() {
const data = await this.docker.listVolumes();
return data.Volumes || [];
const validated = this.validateApiData<any>(data);
return validated.Volumes || [];
}
public async getNetworks() {
return await this.docker.listNetworks();
const data = await this.docker.listNetworks();
return this.validateApiData<any[]>(data);
}
public async removeImage(id: string) {
@@ -118,12 +129,12 @@ class DockerController {
public async getRunningContainers() {
const containers = await this.docker.listContainers({ all: false });
return containers;
return this.validateApiData<any[]>(containers);
}
public async getAllContainers() {
const containers = await this.docker.listContainers({ all: true });
return containers;
return this.validateApiData<any[]>(containers);
}
public async getContainersByStack(stackName: string) {
+3 -2
View File
@@ -86,8 +86,9 @@ export class FileSystemService {
}
return stackNames;
} catch (error) {
console.error('Error reading stacks:', error);
} catch (error: any) {
const nodeName = NodeRegistry.getInstance().getNode(this.nodeId)?.name || 'Unknown';
console.warn(`[SFTP] Failed to fetch stacks for Node ${nodeName}: ${error.message || error}`);
return [];
}
}
+6
View File
@@ -112,6 +112,12 @@ export class NodeRegistry {
try {
const docker = this.createDockerClient(node);
const info = await docker.info();
// Validate the payload contains actual Docker daemon info, not arbitrary HTML
if (!info || !info.OperatingSystem || typeof info.Containers !== 'number') {
throw new Error("Invalid response from Docker API. Did you provide a web port instead of the Docker daemon port?");
}
db.updateNodeStatus(nodeId, 'online');
return {
success: true,
+9 -6
View File
@@ -212,9 +212,12 @@ export function NodeManager() {
value={formData.compose_dir}
onChange={(e) => setFormData({ ...formData, compose_dir: e.target.value })}
/>
<p className="text-xs text-muted-foreground">
The root directory where compose stack folders live on this node
</p>
<div className="text-xs text-muted-foreground space-y-1 mt-1">
<p>The root directory where compose stack folders live on this node.</p>
<p className="text-amber-600 dark:text-amber-400 font-medium">
Note: Strategy B requires valid SSH credentials to read and edit compose.yaml files. TCP alone only grants container lifecycle management.
</p>
</div>
</div>
</div>
);
@@ -238,7 +241,7 @@ export function NodeManager() {
Add Node
</Button>
</DialogTrigger>
<DialogContent>
<DialogContent className="sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Add Remote Node</DialogTitle>
</DialogHeader>
@@ -256,7 +259,7 @@ export function NodeManager() {
<Separator />
{/* Nodes Table */}
<div className="rounded-md border">
<div className="rounded-md border overflow-x-auto w-full">
<Table>
<TableHeader>
<TableRow>
@@ -380,7 +383,7 @@ export function NodeManager() {
{/* Edit Dialog */}
<Dialog open={editOpen} onOpenChange={setEditOpen}>
<DialogContent>
<DialogContent className="sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Edit Node</DialogTitle>
</DialogHeader>