M19: deriveStackName cross-references deployed stacks (fix DB-container misattribution)

deriveStackName pure-suffix-stripped on '-' (postgres/db/mariadb/.../cache), so a
stack whose slug ENDS in a role token (e.g. 'my-cache') was misattributed (stripped
to 'my') — filing its DB dump under the wrong/nonexistent stack. Now threads the set
of deployed stack names (m.knownStackNames() <- ListDeployedStacks) into
DiscoverDatabases and cross-references: candidate suffix-strip if known, else the
container name if it IS a known stack, else longest known stack that is a prefix
(handles <stack>_postgres / <stack>-1), else legacy strip. nil/empty known = legacy
behaviour (appexport passes nil). Table test incl. the my-cache case (fails pre-fix).
This commit is contained in:
2026-06-14 14:08:56 +02:00
parent 2d3bf64eeb
commit 6bab68b132
6 changed files with 142 additions and 11 deletions
+56 -4
View File
@@ -64,7 +64,16 @@ type DumpFileInfo struct {
}
// DiscoverDatabases finds running database containers via docker ps.
func DiscoverDatabases(ctx context.Context, logger *log.Logger, debug bool) ([]DiscoveredDB, error) {
//
// knownStacks is the set of actually-deployed stack names; it is used to attribute each DB container to
// the correct stack (M19). Pass nil/empty for the legacy suffix-strip behaviour.
func DiscoverDatabases(ctx context.Context, logger *log.Logger, debug bool, knownStacks []string) ([]DiscoveredDB, error) {
known := make(map[string]bool, len(knownStacks))
for _, s := range knownStacks {
if s != "" {
known[s] = true
}
}
if debug {
logger.Printf("[DEBUG] DiscoverDatabases: running docker ps to find database containers")
}
@@ -113,7 +122,7 @@ func DiscoverDatabases(ctx context.Context, logger *log.Logger, debug bool) ([]D
ContainerID: id,
ContainerName: name,
DBType: dbType,
StackName: deriveStackName(name),
StackName: deriveStackName(name, known),
}
// Get env vars from container
@@ -640,8 +649,51 @@ func getMariaDBPassword(ctx context.Context, containerID string) string {
return ""
}
// deriveStackName strips known DB suffixes from container name.
func deriveStackName(containerName string) string {
// deriveStackName maps a DB container name to its owning stack name.
//
// M19: the old logic pure-suffix-stripped on `-` (postgres/db/mariadb/mysql/database/redis/cache),
// which misattributes a stack whose real slug ENDS in a role token (e.g. a stack literally named
// `my-cache` → stripped to `my`). When the set of actually-deployed stack names is known, cross-reference
// it so the result is a real stack:
// - candidate := suffix-strip result.
// - known[candidate] → candidate (a real DB-role suffix of a real stack, e.g. romm-postgres→romm).
// - else known[containerName] → containerName (the container name IS the stack — don't strip, e.g. my-cache).
// - else longest known prefix → handles <stack>_postgres / <stack>-1 / compose-suffixed names.
// - else → candidate (fall back to today's suffix-strip; preserves behaviour when
// the stack list is empty/unavailable, so nothing regresses).
// A nil/empty `known` map = the legacy fast path (pure suffix-strip).
func deriveStackName(containerName string, known map[string]bool) string {
candidate := suffixStripStackName(containerName)
if len(known) == 0 {
return candidate
}
if known[candidate] {
return candidate
}
if known[containerName] {
return containerName
}
// Longest known stack name that is a prefix of the container name (tie-break: longest wins).
best := ""
for name := range known {
if name == "" || len(name) >= len(containerName) {
continue
}
// boundary char so "rom" doesn't match "romm-..."; compose/role separators are - or _.
sep := containerName[len(name)]
if strings.HasPrefix(containerName, name) && (sep == '-' || sep == '_') && len(name) > len(best) {
best = name
}
}
if best != "" {
return best
}
return candidate
}
// suffixStripStackName is the legacy pure suffix-strip (the M19 fallback when no stack list is known).
func suffixStripStackName(containerName string) string {
knownSuffixes := []string{"postgres", "db", "mariadb", "mysql", "database", "redis", "cache"}
parts := strings.Split(containerName, "-")