A directory holding anything the mesh did not put there is never removed

Found by asking what the conversion needs, and it is the one failure in
this system that cannot be undone.

Unassigning a module made its directory an orphan, and an orphan
directory was deleted with everything under it — os.RemoveAll — while
the report said "removed". A database's files, a mail spool, somebody's
uploads. Reproduced before fixing: assign a module, let a service write
into its directory, unassign the module, and the file is gone.

Now a directory that still holds something is kept and said so, naming
how many items are in it.

What makes that safe rather than merely cautious is the removal order,
which was already right. Everything the mesh puts in a directory is
itself a declared resource, and orphans are removed in reverse
declaration order — so what the mesh wrote is already gone by the time
the directory is reached. Anything still there was put there by
something else, which is the definition of data.

It is the host's own line applied to the one shape where getting it
wrong does not recover: it removes what it made and leaves what it
merely configured. An empty directory is what it made; a full one is
not, and an empty one is still removed so nothing accumulates.

Files are unchanged. A declared file is the mesh's own, and losing a
config file is not the failure this is about.
This commit is contained in:
2026-08-31 19:53:41 +02:00
parent 4a43e21794
commit f48e06473d
2 changed files with 102 additions and 1 deletions
+31 -1
View File
@@ -601,7 +601,37 @@ func applyService(ctx context.Context, sys system.System, r *declaration.Service
// installed would be describing an effect it declined to have.
func remove(ctx context.Context, sys system.System, a store.Applied, run Runner) (string, string, error) {
switch declaration.Type(a.Type) {
case declaration.TypeFile, declaration.TypeDirectory:
case declaration.TypeDirectory:
// **A directory with anything left in it is kept, and that is the rule that protects
// data.** Everything the mesh put inside is itself a declared resource, and orphans are
// removed in reverse declaration order — so by the time a directory comes to be removed,
// what the mesh wrote there is already gone. Anything still present is something nobody
// declared: a database's files, a mail spool, somebody's uploads.
//
// Before this, unassigning a module deleted its data directory and everything under it,
// and the report said "removed". Nothing anywhere said what had been in there.
//
// This is the host's own line, applied to the one shape where getting it wrong is not
// recoverable: it removes what it made and leaves what it merely configured. An empty
// directory is what it made. A full one is not.
entries, err := os.ReadDir(a.Target)
if errors.Is(err, os.ErrNotExist) {
return "forgotten", "no longer there", nil
}
if err != nil {
return "", "", err
}
if len(entries) > 0 {
return "kept", fmt.Sprintf(
"no longer declared, and %d item(s) inside that the mesh did not put there — "+
"remove it by hand once you know what it is", len(entries)), nil
}
if err := os.Remove(a.Target); err != nil {
return "", "", err
}
return "removed", "no longer declared, and empty", nil
case declaration.TypeFile:
if err := os.RemoveAll(a.Target); err != nil {
return "", "", err
}