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:
@@ -12,11 +12,18 @@ import (
|
||||
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
|
||||
)
|
||||
|
||||
// migrationFlagDir contains per-app flag files written after a successful
|
||||
// per-app migration; presence means the app's legacy -v lines have been
|
||||
// drained into the attachment store. Lives under the registry directory
|
||||
// so it doesn't collide with the property-store path
|
||||
// `config/storage/<appName>`.
|
||||
// MigratedProperty is the per-app marker recording that legacy
|
||||
// docker-options `-v` lines have been drained into named storage
|
||||
// entries plus attachments. Written via the property store so it is
|
||||
// visible to property-store backup/restore tooling, replacing the old
|
||||
// filesystem flag file at `data/storage-registry/migrations/<app>`.
|
||||
const MigratedProperty = "legacy-mounts-migrated"
|
||||
|
||||
// migrationFlagDir / migrationFlagFile remain for one release cycle so
|
||||
// the upgrade-cycle helper convertLegacyMigrationFlag can drain any
|
||||
// leftover flag files into the new property.
|
||||
// TODO(post-deprecation): remove both functions and the
|
||||
// migrationFlagDir entry in triggers.go's install directory list.
|
||||
func migrationFlagDir() string {
|
||||
return filepath.Join(RegistryDirectory(), "migrations")
|
||||
}
|
||||
@@ -25,15 +32,17 @@ func migrationFlagFile(appName string) string {
|
||||
return filepath.Join(migrationFlagDir(), appName)
|
||||
}
|
||||
|
||||
// MigrateApp re-runs the legacy migration for a single app, ignoring
|
||||
// the per-app flag file so an operator can force-rescan an app whose
|
||||
// docker-options state changed after the bulk install-time pass.
|
||||
// Used by `dokku storage:migrate <app>`.
|
||||
// MigrateApp re-runs the legacy migration for a single app so an
|
||||
// operator can force-rescan an app whose docker-options state changed
|
||||
// after the bulk install-time pass. Used by `dokku storage:migrate
|
||||
// <app>`. The marker property is intentionally left in place; migrateApp
|
||||
// only writes it when something was actually drained, so a re-run on
|
||||
// an already-drained app with no new `-v` lines preserves the existing
|
||||
// "had legacy state, drained" signal.
|
||||
func MigrateApp(appName string) error {
|
||||
if err := os.MkdirAll(migrationFlagDir(), 0755); err != nil {
|
||||
return fmt.Errorf("unable to create migration flag dir: %w", err)
|
||||
if err := convertLegacyMigrationFlag(appName); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(migrationFlagFile(appName))
|
||||
if err := migrateApp(appName); err != nil {
|
||||
return fmt.Errorf("storage migration failed for app %q: %w", appName, err)
|
||||
}
|
||||
@@ -42,12 +51,9 @@ func MigrateApp(appName string) error {
|
||||
|
||||
// MigrateLegacyMounts walks every app and converts its legacy `-v`
|
||||
// docker-options entries into named storage entries plus attachments.
|
||||
// Idempotent: per-app flag files short-circuit re-runs.
|
||||
// Idempotent: the per-app legacy-mounts-migrated property short-circuits
|
||||
// re-runs.
|
||||
func MigrateLegacyMounts() error {
|
||||
if err := os.MkdirAll(migrationFlagDir(), 0755); err != nil {
|
||||
return fmt.Errorf("unable to create migration flag dir: %w", err)
|
||||
}
|
||||
|
||||
apps, err := common.DokkuApps()
|
||||
if err != nil {
|
||||
if errors.Is(err, common.NoAppsExist) {
|
||||
@@ -57,7 +63,10 @@ func MigrateLegacyMounts() error {
|
||||
}
|
||||
|
||||
for _, app := range apps {
|
||||
if _, err := os.Stat(migrationFlagFile(app)); err == nil {
|
||||
if err := convertLegacyMigrationFlag(app); err != nil {
|
||||
return err
|
||||
}
|
||||
if common.PropertyExists(PluginName, app, MigratedProperty) {
|
||||
continue
|
||||
}
|
||||
if err := migrateApp(app); err != nil {
|
||||
@@ -67,6 +76,27 @@ func MigrateLegacyMounts() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertLegacyMigrationFlag drains the legacy filesystem flag file
|
||||
// (data/storage-registry/migrations/<app>) into the new
|
||||
// legacy-mounts-migrated property and removes the file. Runs as part
|
||||
// of the per-app loop so installs upgrading from the previous release
|
||||
// surface the marker in the property store without re-running the
|
||||
// drain logic. No-op when the flag file is absent.
|
||||
// TODO(post-deprecation): remove this helper and its callers.
|
||||
func convertLegacyMigrationFlag(appName string) error {
|
||||
path := migrationFlagFile(appName)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := common.PropertyWrite(PluginName, appName, MigratedProperty, "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
// migrateApp performs the per-app migration. Order is intentional: write
|
||||
// the entry, write the attachment, drain the docker-options line. Any
|
||||
// step failing aborts before drain, so the original behavior is
|
||||
@@ -99,13 +129,20 @@ func migrateApp(appName string) error {
|
||||
}
|
||||
sort.Strings(mounts)
|
||||
|
||||
if len(mounts) == 0 {
|
||||
// No legacy `-v` lines for this app. Leave the marker unset so
|
||||
// `storage:report` (and future tooling) distinguishes apps that
|
||||
// never had legacy state from apps that did and were drained.
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, mount := range mounts {
|
||||
if err := migrateMount(appName, mount, phaseMap[mount]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return touchMigrationFlag(appName)
|
||||
return common.PropertyWrite(PluginName, appName, MigratedProperty, "true")
|
||||
}
|
||||
|
||||
func migrateMount(appName string, mount string, phases []string) error {
|
||||
@@ -167,10 +204,6 @@ func migrateMount(appName string, mount string, phases []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func touchMigrationFlag(appName string) error {
|
||||
return common.TouchFile(migrationFlagFile(appName))
|
||||
}
|
||||
|
||||
func filterMountLines(lines []string) []string {
|
||||
out := []string{}
|
||||
for _, line := range lines {
|
||||
|
||||
@@ -112,9 +112,9 @@ func TestMigrateAppSinglePhase(t *testing.T) {
|
||||
t.Errorf("deploy options after migration = %v, want [--restart=on-failure:5]", deployAfter)
|
||||
}
|
||||
|
||||
// Migration flag file is in place.
|
||||
if _, err := os.Stat(migrationFlagFile("alpha")); err != nil {
|
||||
t.Errorf("migration flag missing: %v", err)
|
||||
// Property marker is in place.
|
||||
if !common.PropertyExists(PluginName, "alpha", MigratedProperty) {
|
||||
t.Errorf("legacy-mounts-migrated property missing")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,8 +230,8 @@ func TestMigrateLegacyMountsBulkAndFlagFastPath(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, app := range []string{"alpha", "beta"} {
|
||||
if _, err := os.Stat(migrationFlagFile(app)); err != nil {
|
||||
t.Errorf("flag file missing for %s: %v", app, err)
|
||||
if !common.PropertyExists(PluginName, app, MigratedProperty) {
|
||||
t.Errorf("legacy-mounts-migrated property missing for %s", app)
|
||||
}
|
||||
atts, err := LoadAttachments(app)
|
||||
if err != nil {
|
||||
@@ -242,9 +242,9 @@ func TestMigrateLegacyMountsBulkAndFlagFastPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Re-stage a -v on alpha and rerun MigrateLegacyMounts. The flag
|
||||
// file short-circuits per-app migration, so the new line stays in
|
||||
// docker-options.
|
||||
// Re-stage a -v on alpha and rerun MigrateLegacyMounts. The
|
||||
// property marker short-circuits per-app migration, so the new
|
||||
// line stays in docker-options.
|
||||
if err := common.PropertyListWrite("docker-options", "alpha", "_default_.deploy", []string{"-v /sneaky:/x"}); err != nil {
|
||||
t.Fatalf("re-stage alpha: %v", err)
|
||||
}
|
||||
@@ -324,6 +324,91 @@ func TestMigrateAppChownsLegacyEntryFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigrateAppNoMountsDoesNotMarkMigrated confirms that an app with
|
||||
// no legacy `-v` lines leaves the legacy-mounts-migrated property
|
||||
// unset. Apps that have never had legacy state are distinguishable
|
||||
// from apps that did and were drained.
|
||||
func TestMigrateAppNoMountsDoesNotMarkMigrated(t *testing.T) {
|
||||
_, dokkuRoot := setupMigrationEnv(t)
|
||||
stageApp(t, dokkuRoot, "alpha", map[string][]string{
|
||||
"deploy": {"--restart=on-failure:5"},
|
||||
})
|
||||
|
||||
if err := migrateApp("alpha"); err != nil {
|
||||
t.Fatalf("migrateApp: %v", err)
|
||||
}
|
||||
|
||||
if common.PropertyExists(PluginName, "alpha", MigratedProperty) {
|
||||
t.Errorf("legacy-mounts-migrated property should not be set for an app with no -v lines")
|
||||
}
|
||||
|
||||
atts, err := LoadAttachments("alpha")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadAttachments: %v", err)
|
||||
}
|
||||
if len(atts) != 0 {
|
||||
t.Errorf("expected 0 attachments, got %d", len(atts))
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigrateLegacyMountsConvertsLegacyFlagFile is the upgrade-cycle
|
||||
// regression: on an install upgrading from the previous release, the
|
||||
// per-app filesystem flag file under data/storage-registry/migrations/
|
||||
// must be drained into the property store and removed.
|
||||
func TestMigrateLegacyMountsConvertsLegacyFlagFile(t *testing.T) {
|
||||
_, dokkuRoot := setupMigrationEnv(t)
|
||||
stageApp(t, dokkuRoot, "alpha", map[string][]string{
|
||||
"deploy": {"--restart=on-failure:5"},
|
||||
})
|
||||
|
||||
flagPath := migrationFlagFile("alpha")
|
||||
if err := common.TouchFile(flagPath); err != nil {
|
||||
t.Fatalf("touch flag file: %v", err)
|
||||
}
|
||||
|
||||
if err := MigrateLegacyMounts(); err != nil {
|
||||
t.Fatalf("MigrateLegacyMounts: %v", err)
|
||||
}
|
||||
|
||||
got := common.PropertyGetDefault(PluginName, "alpha", MigratedProperty, "")
|
||||
if got != "true" {
|
||||
t.Errorf("legacy-mounts-migrated = %q, want %q", got, "true")
|
||||
}
|
||||
if _, err := os.Stat(flagPath); !os.IsNotExist(err) {
|
||||
t.Errorf("expected flag file gone, got err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigrateLegacyMountsRespectsExistingProperty confirms that the
|
||||
// property is the gate: an app with a `-v` line is skipped when the
|
||||
// property is already set, leaving the legacy line in docker-options.
|
||||
func TestMigrateLegacyMountsRespectsExistingProperty(t *testing.T) {
|
||||
_, dokkuRoot := setupMigrationEnv(t)
|
||||
stageApp(t, dokkuRoot, "alpha", map[string][]string{
|
||||
"deploy": {"-v /var/log:/log"},
|
||||
})
|
||||
|
||||
if err := common.PropertyWrite(PluginName, "alpha", MigratedProperty, "true"); err != nil {
|
||||
t.Fatalf("seed property: %v", err)
|
||||
}
|
||||
|
||||
if err := MigrateLegacyMounts(); err != nil {
|
||||
t.Fatalf("MigrateLegacyMounts: %v", err)
|
||||
}
|
||||
|
||||
got := phaseOptions(t, "alpha", "deploy")
|
||||
if !equalSorted(got, []string{"-v /var/log:/log"}) {
|
||||
t.Errorf("expected -v line preserved, got %v", got)
|
||||
}
|
||||
atts, err := LoadAttachments("alpha")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadAttachments: %v", err)
|
||||
}
|
||||
if len(atts) != 0 {
|
||||
t.Errorf("expected 0 attachments (property gated migration), got %d", len(atts))
|
||||
}
|
||||
}
|
||||
|
||||
func equalSorted(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
|
||||
@@ -37,7 +37,10 @@ func TriggerInstall() error {
|
||||
|
||||
// The install trigger runs as root; the directories it just made are
|
||||
// root-owned, but the dokku user is the one that runs storage:create
|
||||
// and writes entry / migration-flag files into them.
|
||||
// and writes entry files into them. migrationFlagDir() stays in the
|
||||
// list for one release cycle so the upgrade-cycle conversion in
|
||||
// MigrateLegacyMounts can read pre-existing flag files.
|
||||
// TODO(post-deprecation): drop migrationFlagDir() from this list.
|
||||
for _, dir := range []string{RegistryDirectory(), EntriesDirectory(), migrationFlagDir()} {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("Unable to create %s: %s", dir, err.Error())
|
||||
|
||||
Reference in New Issue
Block a user