fix: chown migrated legacy storage entries to dokku

`SaveEntry` and `touchMigrationFlag` wrote files via `os.WriteFile` and `os.Create` and never chowned them, so the install-time legacy-mount migration produced root-owned `legacy-*.json` files in `/var/lib/dokku/data/storage-registry/entries/`. The dokku user that runs `ps:rebuild` could not read them and every rebuild on a 0.37.x to 0.38.0 upgrade failed with permission denied. The companion `repairRegistryOwnership` pass in `TriggerInstall` rewrites ownership across the whole registry tree so installs that already ran the buggy code are fixed on the next package upgrade, since the per-app migration flag would otherwise cause `MigrateLegacyMounts` to skip the broken files forever.
This commit is contained in:
Jose Diaz-Gonzalez
2026-05-02 05:37:11 -04:00
parent cdfcd4d2c8
commit 179d09fdd5
7 changed files with 168 additions and 24 deletions

View File

@@ -5,36 +5,26 @@ import (
"os/user"
"path/filepath"
"sort"
"strconv"
"syscall"
"testing"
"github.com/dokku/dokku/plugins/common"
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
)
// setupMigrationEnv points DOKKU_LIB_ROOT and DOKKU_ROOT at temp dirs and
// tells the permission helpers to chown to the current user (a no-op),
// so the test works without root and without the dokku system user.
// Mirrors the helper in docker-options/migration_test.go.
// setupMigrationEnv reuses withTempLibRoot for DOKKU_LIB_ROOT plus the
// permission-helper user/group overrides, then layers DOKKU_ROOT and
// PLUGIN_PATH on top so DokkuApps can find the staged apps. Mirrors the
// docker-options migration test fixture.
func setupMigrationEnv(t *testing.T) (libRoot string, dokkuRoot string) {
t.Helper()
libRoot = t.TempDir()
libRoot = withTempLibRoot(t)
dokkuRoot = t.TempDir()
t.Setenv("DOKKU_LIB_ROOT", libRoot)
t.Setenv("DOKKU_ROOT", dokkuRoot)
t.Setenv("PLUGIN_PATH", filepath.Join(libRoot, "plugins"))
current, err := user.Current()
if err != nil {
t.Fatalf("user.Current: %v", err)
}
group, err := user.LookupGroupId(current.Gid)
if err != nil {
t.Fatalf("user.LookupGroupId: %v", err)
}
t.Setenv("DOKKU_SYSTEM_USER", current.Username)
t.Setenv("DOKKU_SYSTEM_GROUP", group.Name)
// MigrateLegacyMounts creates the flag dir before iterating apps;
// migrateApp (the per-app helper) assumes it already exists. Pre-
// create it so tests that call migrateApp directly don't trip.
@@ -300,6 +290,40 @@ func TestMigrateAppRefusesEntryNameCollision(t *testing.T) {
}
}
// TestMigrateAppChownsLegacyEntryFiles is the regression for #8557:
// migrating from the install trigger (which runs as root) must produce
// dokku-owned legacy-*.json files. setupMigrationEnv overrides the
// system user/group to the current process user, so we assert against
// that uid rather than the literal dokku one.
func TestMigrateAppChownsLegacyEntryFiles(t *testing.T) {
_, dokkuRoot := setupMigrationEnv(t)
stageApp(t, dokkuRoot, "alpha", map[string][]string{
"deploy": {"-v /var/log:/log"},
})
if err := migrateApp("alpha"); err != nil {
t.Fatalf("migrateApp: %v", err)
}
entry := LegacyMountToEntry("/var/log:/log")
info, err := os.Stat(entryPath(entry.Name))
if err != nil {
t.Fatalf("stat entry: %v", err)
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
t.Fatalf("stat.Sys is not *syscall.Stat_t")
}
current, err := user.Current()
if err != nil {
t.Fatalf("user.Current: %v", err)
}
if got := strconv.Itoa(int(stat.Uid)); got != current.Uid {
t.Errorf("entry file uid = %s, want %s", got, current.Uid)
}
}
func equalSorted(a, b []string) bool {
if len(a) != len(b) {
return false