mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-30 20:29:15 +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:
@@ -470,6 +470,29 @@ const newVolume: PreflightRule = {
|
||||
},
|
||||
};
|
||||
|
||||
const anonymousVolume: PreflightRule = {
|
||||
id: 'anonymous-volume',
|
||||
run(ctx) {
|
||||
if (!ctx.model) return [];
|
||||
const findings: PreflightFinding[] = [];
|
||||
for (const svc of ctx.model.services) {
|
||||
const anon = (svc.storageMounts ?? []).filter(m => m.type === 'anonymous');
|
||||
if (anon.length === 0) continue;
|
||||
const targets = anon.map(m => m.target).filter(Boolean);
|
||||
findings.push({
|
||||
ruleId: 'anonymous-volume',
|
||||
severity: 'info',
|
||||
title: 'Anonymous volume in use',
|
||||
message: `Service "${svc.name}" mounts ${anon.length > 1 ? `${anon.length} anonymous volumes` : 'an anonymous volume'}${targets.length ? ` at ${targets.join(', ')}` : ''}. Anonymous volumes have no name, so they are easy to miss when backing up and are orphaned when the container is recreated.`,
|
||||
sourcePath: svc.name,
|
||||
service: svc.name,
|
||||
remediation: 'Give the volume a name so it can be referenced, backed up, and reattached.',
|
||||
});
|
||||
}
|
||||
return findings;
|
||||
},
|
||||
};
|
||||
|
||||
const containerNameInternalDup: PreflightRule = {
|
||||
id: 'container-name-internal-dup',
|
||||
run(ctx) {
|
||||
@@ -704,6 +727,7 @@ export const PREFLIGHT_RULES: PreflightRule[] = [
|
||||
externalVolumeMissing,
|
||||
newNetwork,
|
||||
newVolume,
|
||||
anonymousVolume,
|
||||
containerNameInternalDup,
|
||||
containerNameCollision,
|
||||
exposureInternalPublished,
|
||||
|
||||
Reference in New Issue
Block a user