mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
fix: harden docker api validation, handle sftp errors, and fix node manager ui
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user