refactor: convert filesystem migration markers to plugin properties

The docker-options DOCKER_OPTIONS_<PHASE>.migrated sentinels and the storage data/storage-registry/migrations/<app> flag files are now stored as per-app properties (`migrated-<phase>` and `legacy-mounts-migrated` respectively), so they survive a property-store backup/restore and align with the existing `migrated-from-files`, `env-migrated`, and `nginx-conf-sigil-migrated` patterns. The new markers are written only when a non-empty legacy source was actually migrated, distinguishing apps that never had legacy state from apps that did and were drained. A one-cycle upgrade-cycle conversion drains any leftover filesystem sentinels into the new properties and removes the files; for docker-options the conversion deliberately runs before the global `migrated-from-files` short-circuit so installs upgrading from the previous release still get their `.migrated` files converted.
This commit is contained in:
Jose Diaz-Gonzalez
2026-05-23 23:47:58 -04:00
parent 5df399273a
commit b2320121fe
9 changed files with 381 additions and 59 deletions

View File

@@ -245,22 +245,66 @@ func CommandReport(appName string, format string, infoFlag string) error {
return ReportSingleApp(appName, format, infoFlag)
}
// migrateLegacyDockerOptionsFiles converts pre-properties DOCKER_OPTIONS_*
// files into property lists. It is gated by a single global marker so it never
// re-runs - even if a user later restores a DOCKER_OPTIONS_* file by hand.
func migrateLegacyDockerOptionsFiles() error {
if common.PropertyExists("docker-options", "--global", "migrated-from-files") {
// migratedPropertyKey returns the per-app property name recording that
// an app's DOCKER_OPTIONS_<PHASE> file was drained into the property
// store. Surfaces in the property store as `docker-options/<app>/migrated-<phase>`.
func migratedPropertyKey(phase string) string {
return "migrated-" + strings.ToLower(phase)
}
// convertLegacyMigratedMarker drains a leftover DOCKER_OPTIONS_<PHASE>.migrated
// sentinel into the new per-phase property and removes the file. Runs
// as part of the upgrade-cycle pass; no-op when the file is absent.
// TODO(post-deprecation): remove this helper and its caller.
func convertLegacyMigratedMarker(appName, phase string) error {
migratedPath := filepath.Join(common.AppRoot(appName), "DOCKER_OPTIONS_"+strings.ToUpper(phase)+".migrated")
if !common.FileExists(migratedPath) {
return nil
}
if err := common.PropertyWrite("docker-options", appName, migratedPropertyKey(phase), "true"); err != nil {
return err
}
return os.Remove(migratedPath)
}
// migrateLegacyDockerOptionsFiles converts pre-properties
// DOCKER_OPTIONS_* files into property lists. Idempotency comes from
// two layers: a per-phase per-app `migrated-<phase>` property recording
// what was drained, and a global `migrated-from-files` short-circuit so
// the steady-state install path returns immediately.
//
// The upgrade-cycle conversion of leftover `.migrated` filesystem
// sentinels runs BEFORE the global gate so users upgrading from the
// previous release - who have `migrated-from-files` set AND `.migrated`
// files on disk - still get those files drained into properties.
func migrateLegacyDockerOptionsFiles() error {
apps, err := common.DokkuApps()
if err != nil {
if errors.Is(err, common.NoAppsExist) {
return common.PropertyWrite("docker-options", "--global", "migrated-from-files", "true")
if !common.PropertyExists("docker-options", "--global", "migrated-from-files") {
return common.PropertyWrite("docker-options", "--global", "migrated-from-files", "true")
}
return nil
}
return err
}
// Upgrade-cycle conversion: drain any `.migrated` sentinels left
// behind by the previous release into the new per-phase properties.
// Always runs; cheap when nothing to do.
// TODO(post-deprecation): remove this loop.
for _, appName := range apps {
for _, phase := range availablePhases {
if err := convertLegacyMigratedMarker(appName, phase); err != nil {
return err
}
}
}
if common.PropertyExists("docker-options", "--global", "migrated-from-files") {
return nil
}
for _, appName := range apps {
for _, phase := range availablePhases {
legacyPath := filepath.Join(common.AppRoot(appName), "DOCKER_OPTIONS_"+strings.ToUpper(phase))
@@ -273,16 +317,19 @@ func migrateLegacyDockerOptionsFiles() error {
return err
}
if err := common.PropertyListWrite("docker-options", appName, propertyKey(DefaultProcessType, phase), lines); err != nil {
return err
if len(lines) > 0 {
if err := common.PropertyListWrite("docker-options", appName, propertyKey(DefaultProcessType, phase), lines); err != nil {
return err
}
if err := common.PropertyWrite("docker-options", appName, migratedPropertyKey(phase), "true"); err != nil {
return err
}
common.LogInfo1(fmt.Sprintf("Migrated %s to docker-options properties", legacyPath))
}
migratedPath := legacyPath + ".migrated"
if err := os.Rename(legacyPath, migratedPath); err != nil {
return fmt.Errorf("Unable to rename migrated file %s: %s", legacyPath, err.Error())
if err := os.Remove(legacyPath); err != nil {
return fmt.Errorf("Unable to remove migrated file %s: %s", legacyPath, err.Error())
}
common.LogInfo1(fmt.Sprintf("Migrated %s to docker-options properties", legacyPath))
}
}
@@ -312,6 +359,7 @@ func readLegacyOptionsFile(path string) ([]string, error) {
// removeMigratedLegacyFiles deletes any leftover DOCKER_OPTIONS_*.migrated
// files for an app. Called from post-delete so we don't leak files into a
// directory that is about to be torn down anyway.
// TODO(post-deprecation): remove this helper and the call in TriggerPostDelete.
func removeMigratedLegacyFiles(appName string) {
for _, phase := range availablePhases {
path := filepath.Join(common.AppRoot(appName), "DOCKER_OPTIONS_"+strings.ToUpper(phase)+".migrated")