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:
Anso
2026-06-20 15:06:26 -04:00
committed by GitHub
parent 57a0856ffc
commit 9ea2864d60
26 changed files with 1591 additions and 12 deletions
+19
View File
@@ -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.