mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-24 08:57:25 +00:00
feat: per-stack storage inventory and portability guardrails (#1399)
* feat: per-stack storage inventory and portability guardrails Add a Storage tab to the stack Anatomy panel that derives a per-stack mount inventory (bind mounts, named/anonymous volumes, tmpfs, docker socket; read-only vs read-write; host-path existence, type, and owner) from the effective Compose model, and classifies the stack as Portable, Partially portable, Node-bound, or Unknown with the reasons behind it. - New GET /api/stacks/:stackName/storage route (stack:read, Community), served by an on-demand, non-persisted service that renders the effective model, probes within-stack bind sources (symlink-escape aware), and runs the deterministic portability classifier. - Extend the effective-model parser additively with a full per-mount inventory and service-level tmpfs, leaving the rule-facing binds/namedVolumes byte-identical for the existing preflight rules. - New anonymous-volume preflight finding. - Admin-visible "no recent snapshot" warning that reuses the existing hub-local snapshot-coverage endpoint, plus a static note distinguishing config snapshots from application-data backups. - Surface storage assumptions in the Stack Dossier markdown export. - Gate the tab behind a new compose-storage capability on both sides. * docs: phrase the Storage tab availability as current behavior Replace the "older Sencho version / until it is updated" wording in the Storage feature page with present-tense, capability-based phrasing.
This commit is contained in:
@@ -17,6 +17,7 @@ import { buildStackDriftReport, type DriftFindingKind, type StackDriftReport } f
|
||||
import { DriftLedgerService, type DriftTemporal } from '../services/DriftLedgerService';
|
||||
import { ComposeDoctorService } from '../services/ComposeDoctorService';
|
||||
import { buildStackNetworkFacts } from '../services/network/composeNetworkInspector';
|
||||
import { buildStorageInventory } from '../services/storage/inventory';
|
||||
import { buildEffectiveAnatomy } from '../services/effectiveAnatomy';
|
||||
import { buildEnvInventory } from '../services/EnvInventoryService';
|
||||
import { EXPOSURE_INTENTS, type ExposureIntent } from '../services/network/types';
|
||||
@@ -1094,6 +1095,24 @@ stacksRouter.get('/:stackName/networking', async (req: Request, res: Response) =
|
||||
}
|
||||
});
|
||||
|
||||
// Storage inventory: per-stack mount inventory (binds, named/anonymous volumes,
|
||||
// tmpfs, docker socket; read-only vs read-write; host-path existence/type/owner)
|
||||
// and a portability verdict derived from the effective model + within-stack
|
||||
// host-path probes. Read-only and advisory; auto-proxies to the active node.
|
||||
// Never returns raw render stderr or any environment value.
|
||||
stacksRouter.get('/:stackName/storage', async (req: Request, res: Response) => {
|
||||
const stackName = req.params.stackName as string;
|
||||
if (!requirePermission(req, res, 'stack:read', 'stack', stackName)) return;
|
||||
if (!(await requireStackExists(req.nodeId, stackName, res))) return;
|
||||
try {
|
||||
res.json(await buildStorageInventory(req.nodeId, stackName));
|
||||
} catch (error) {
|
||||
console.error('[Stacks] Failed to build storage inventory for %s:', sanitizeForLog(stackName),
|
||||
sanitizeForLog(inspect(error, { depth: 4 })));
|
||||
res.status(500).json({ error: 'Failed to build storage inventory' });
|
||||
}
|
||||
});
|
||||
|
||||
// Effective Stack Anatomy: structural facts (services, ports, volumes, networks,
|
||||
// restart) from the fully-merged effective model, so a multi-file Git source's
|
||||
// dossier and doc-drift reflect every override file, not just the root compose.
|
||||
|
||||
Reference in New Issue
Block a user