Merge pull request #8907 from dokku/8875-0-38-migration-plugins-ordered-before-config-silently-fail-to-migrate-their-dokku-config-vars

fix: migrate env files before reading deprecated vars
This commit is contained in:
Jose Diaz-Gonzalez
2026-08-08 01:17:34 -04:00
committed by GitHub
11 changed files with 455 additions and 100 deletions

View File

@@ -15,7 +15,7 @@
}
```
- The path on disk to both the global `ENV` file and app `ENV` files have been moved. Users should reference environment variables via the provided plugin triggers rather than directly sourcing the ENV files. Existing ENV files are left untouched and will be removed on the subsequent Dokku install.
- The path on disk to both the global `ENV` file and app `ENV` files have been moved. Users should reference environment variables via the provided plugin triggers rather than directly sourcing the ENV files. Existing ENV files are merged into the new location and removed once they have been drained. **Changed in 0.38.26:** removal previously happened on the subsequent Dokku install for app ENV files, and never happened at all for the global ENV file. An ENV file that reappears at the old path afterwards is treated as a hand-edit: its contents are merged into the new location and the keys are named in a warning rather than being discarded.
- During a fresh apt install, the upstream nginx default vhost files (`/etc/nginx/sites-enabled/default`, `/etc/nginx/sites-available/default`, and `/etc/nginx/conf.d/default.conf`) are renamed to `${path}.dokku-disabled` (not deleted) to avoid a `duplicate default server for 0.0.0.0:80` error. Operators with local customizations can recover them by inspecting the `.dokku-disabled` siblings. Upgrade-in-place installs do not touch any existing nginx files.
- Fresh apt installs now ship a catch-all default site at `/etc/nginx/conf.d/00-default-vhost.conf` that rejects requests with unknown Host headers using `ssl_reject_handshake on` (HTTPS) and `return 444` (HTTP). This replaces the manual workaround previously documented in the nginx docs. The behavior can be opted out at install time via the `dokku/install_default_site` debconf prompt. On nginx older than 1.19.4 (e.g., Debian Bullseye's nginx 1.18.0), the postinst installs an HTTP-only variant of the catch-all that omits the SSL listener and `ssl_reject_handshake`, since that directive is unsupported on those versions. See the [Default site documentation](/docs/networking/proxies/nginx.md#default-site).
- The `docker-local` scheduler now sends `SIGTERM` to old containers immediately after a successful deploy, rather than waiting `wait-to-retire` seconds before signaling. This matches Heroku's graceful-shutdown contract and lets applications begin draining in-flight work as soon as proxy traffic switches. The `wait-to-retire` grace period and `stop-timeout-seconds` hard-stop continue to apply as before. See the [zero downtime deploys documentation](/docs/deployment/zero-downtime-deploys.md#wait-to-retire) for more details.
@@ -28,6 +28,7 @@
- The `scheduler-k3s` plugin now manages env config and the dokku-generated image pull Secret as their own helm releases with stable names (`config-{app}` and `pull-secret-{app}`) rather than bundling them into the app helm chart with a per-deploy timestamp suffix (`env-{app}.{ts}` / `ims-{app}.{ts}`). This fixes two bugs: a helm rollback of the app chart no longer deletes Secrets that older ReplicaSets still reference, and the Deployment's `imagePullSecrets` list no longer accumulates references to nonexistent Secrets across deploys. The next deploy of an app switches the Deployment's `envFrom` and `imagePullSecrets` references to the stable names and prunes any leaked entries; existing live Deployments do not need to be patched manually. App rename now also uninstalls the old `tls-{app}`, `config-{app}`, and `pull-secret-{app}` releases under the previous app name; the new name's releases are recreated on the next deploy or certs sync.
- **New in 0.38.25:** Values supplied through docker options, `dokku run`'s `-e`/`--env` flag, and `--ttl-seconds` are no longer evaluated by the shell when assembling a container's arguments; they are now tokenized and passed through verbatim. This closes a command-injection vector where a `$(...)` or backtick expression in one of these values executed on the host as the `dokku` user during build, deploy, or run. As a result, shell metacharacters such as `$(...)`, backticks, `$VAR`, and globs in these values are treated literally instead of being expanded, and `--ttl-seconds` must now be a plain integer. Existing Traefik docker-options labels (those whose label key begins with `traefik.`) whose backticks were stored with a stray backslash are repaired automatically the first time `dokku` runs after the upgrade, so they become valid on the next deploy.
- **New in 0.38.26:** Docker options drained out of the pre-0.38.0 `DOCKER_OPTIONS_<PHASE>` files were copied verbatim rather than being re-serialized the way `docker-options:add` stores them, so `docker-options:remove` compared the canonical string it builds against a value that could never match it and exited successfully without removing anything. Removal now matches stored options by shell word instead of by exact string, so those entries can be removed with the value as originally written. Stored options are also rewritten into the canonical form the first time `dokku` runs after the upgrade: values whose shell metacharacters were left unquoted are quoted, and an entry that carried several flags on one line becomes one entry per flag, which additionally fixes `ps:report` reading a restart policy off such a line and the `k3s` scheduler translating only the first of several `--cap-add`/`--sysctl` flags. Options added through `docker-options:add` are already canonical and are left untouched.
- **New in 0.38.26:** Deprecated `DOKKU_*` config variables belonging to plugins whose install step runs before the `config` plugin's were not migrated to their plugin property on the upgrade run. Install steps fire in alphabetical order, so `apps`, `builder`, and `checks` read an app's environment before the `config` plugin had relocated the `ENV` file, found nothing, and moved nothing - without reporting anything. `dokku config:show` kept listing the variable while the plugin behaved as though it were unset, which for `DOKKU_CHECKS_SKIPPED` meant a process type silently regained a health check it was meant to skip. The relocation is now performed before any deprecated variable is read, regardless of install order. Affected installs recover on their next `dokku plugin:install --core`, which the upgrade already runs; the variables listed below can also be re-applied by hand using their replacement command.
- The storage plugin now treats persistent volumes as named, scheduler-aware first-class resources via `storage:create`, `storage:mount`, `storage:set`, and `storage:destroy`. The legacy `storage:mount <app> <host>:<container>` colon form continues to work on docker-local apps but is deprecated; on k3s apps it is rejected. Existing colon-form mounts are migrated automatically the first time the new storage plugin runs (during the install trigger) - they appear as `legacy-<hash>` entries in `storage:list-entries`. The migration is idempotent and tied to a per-app flag file at `$DOKKU_LIB_ROOT/config/storage/.migrated/<app>`; deleting that file forces a re-scan on the next install. The `storage:ensure-directory` command keeps working but now emits a deprecation warning - prefer `storage:create <name> [<path>]` (the path defaults to the same `$DOKKU_LIB_ROOT/data/storage/<name>` location). Storage entry names must now be DNS-1123 labels of 45 characters or less so they can be used verbatim as Helm release and Kubernetes resource names; underscores and uppercase characters that the older `ensure-directory` validator accepted are rejected for new names. The migration synthesizer always uses lowercase hex hashes so existing data is never locked out.
### TLS handshake behavior change

View File

@@ -151,4 +151,4 @@ The following property is recorded internally by the config plugin and is not ex
| Property | Description | Source |
|---|---|---|
| `env-migrated` | Migration sentinel that records the per-app or global `DOKKU_*` env-var → property migration completed for 0.38.0 | `plugins/config/triggers.go` writes `"true"` after the upgrade-time pass |
| `env-migrated` | Migration sentinel that records that the per-app or global `ENV` file has been drained out of its pre-0.38.0 location into the config path. The old file is removed as soon as it has been drained, so one that reappears afterwards was written by hand rather than through `dokku config:*`; its contents are merged in and named in a warning on the next migration pass | `plugins/config/migrate.go` writes `"true"` once the file has been drained |

View File

@@ -581,6 +581,21 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
# TODO
```
### `config-migrate-env`
- Description: Drains the pre-0.38 `$DOKKU_ROOT/ENV` and `$DOKKU_ROOT/<app>/ENV` files into the config property path, removing each file once it has been drained. Idempotent, and safe to call from an install trigger that runs before the config plugin's own.
- Invoked by: `common` when migrating deprecated config vars to plugin properties, checks plugin
- Arguments: none
- Example:
```shell
#!/usr/bin/env bash
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
plugn trigger config-migrate-env
```
### `config-set`
- Description: Sets one or more config values for an app without restarting (when --no-restart flag is specified)

View File

@@ -60,6 +60,11 @@ trigger-checks-install() {
declare trigger="install"
fn-plugin-property-setup "checks"
# the checks plugin installs before the config plugin, so the pre-0.38 ENV
# files have to be drained before any deprecated config var is read
plugn trigger config-migrate-env
migrate_checks_vars_0_5_0 "$@"
migrate_checks_vars_0_6_0 "$@"

View File

@@ -668,6 +668,8 @@ type MigrateConfigEntry struct {
// across all apps and optionally globally. It is idempotent: if the property already
// exists, the migration is skipped for that app/entry.
func MigrateConfigToProperties(pluginName string, entries []MigrateConfigEntry) error {
migrateLegacyEnvFiles()
apps, err := UnfilteredDokkuApps()
if err != nil && !errors.Is(err, NoAppsExist) {
return nil
@@ -694,6 +696,24 @@ func MigrateConfigToProperties(pluginName string, entries []MigrateConfigEntry)
return nil
}
// migrateLegacyEnvFiles drains the pre-0.38 ENV files into the config property
// path before any config var is read.
//
// Install triggers fire in lexicographic order of the enabled-plugin directory
// names, so plugins sorting before "config" - apps, builder and checks among
// them - would otherwise read an environment the config plugin has not
// relocated yet, find it empty, and migrate nothing without reporting anything.
// A failure here is not fatal: the migration is retried on the next install, so
// an unavailable trigger degrades to the previous behavior rather than aborting
// every plugin's install.
func migrateLegacyEnvFiles() {
if _, err := CallPlugnTrigger(PlugnTriggerInput{
Trigger: "config-migrate-env",
}); err != nil {
LogWarn(fmt.Sprintf("Unable to migrate legacy env files: %s", err.Error()))
}
}
// migrateConfigEntry migrates a single config variable to a property for a given app
func migrateConfigEntry(pluginName string, appName string, configVar string, entry MigrateConfigEntry) error {
if entry.ListProperty {

View File

@@ -1,6 +1,6 @@
GOARCH ?= amd64
SUBCOMMANDS = subcommands/bundle subcommands/clear subcommands/export subcommands/get subcommands/import subcommands/keys subcommands/show subcommands/set subcommands/unset
TRIGGERS = triggers/config-export triggers/config-get triggers/config-get-global triggers/install triggers/config-set triggers/config-unset triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-create triggers/post-delete
TRIGGERS = triggers/config-export triggers/config-get triggers/config-get-global triggers/config-migrate-env triggers/install triggers/config-set triggers/config-unset triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-create triggers/post-delete
BUILD = commands config_sub subcommands triggers
PLUGIN_NAME = config

139
plugins/config/migrate.go Normal file
View File

@@ -0,0 +1,139 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/dokku/dokku/plugins/common"
)
// envMigratedProperty records that an ENV file has been drained out of the
// pre-0.38 location into the config property path.
const envMigratedProperty = "env-migrated"
// MigrateEnvFiles drains the pre-0.38 ENV files - $DOKKU_ROOT/ENV and
// $DOKKU_ROOT/<app>/ENV - into the config property path, removing each file as
// soon as it has been drained.
//
// It is safe to call from any plugin's install trigger, and does so via the
// config-migrate-env plugin trigger. Plugins whose enabled-directory name sorts
// before "config" - apps, builder and checks among them - run their deprecated
// DOKKU_* config var to property migrations before the config plugin's own
// install trigger would have relocated these files, and would otherwise read an
// empty environment and silently migrate nothing. Nothing here may assume the
// config install trigger has already run.
func MigrateEnvFiles() error {
if err := common.PropertySetup("config"); err != nil {
return fmt.Errorf("Unable to setup config properties: %s", err.Error())
}
if err := migrateGlobalEnv(); err != nil {
return fmt.Errorf("Unable to migrate global environment: %s", err.Error())
}
apps, err := common.UnfilteredDokkuApps()
if err != nil {
return nil
}
for _, appName := range apps {
if err := migrateAppEnv(appName); err != nil {
return fmt.Errorf("Unable to migrate environment for %s: %s", appName, err.Error())
}
}
return nil
}
// migrateGlobalEnv drains $DOKKU_ROOT/ENV into the global config property path.
func migrateGlobalEnv() error {
if err := common.PropertySetup("--global"); err != nil {
return fmt.Errorf("Unable to setup global environment: %s", err.Error())
}
oldGlobalEnvFile := filepath.Join(common.MustGetEnv("DOKKU_ROOT"), "ENV")
globalEnv, err := LoadGlobalEnv()
if err != nil {
return fmt.Errorf("Unable to load global environment: %s", err.Error())
}
return drainLegacyEnvFile("--global", oldGlobalEnvFile, globalEnv)
}
// migrateAppEnv drains $DOKKU_ROOT/<app>/ENV into the app's config property path.
func migrateAppEnv(appName string) error {
if err := common.PropertySetupApp("config", appName); err != nil {
return fmt.Errorf("Unable to setup app environment: %s", err.Error())
}
if err := setupAppConfigDir(appName); err != nil {
return fmt.Errorf("Unable to setup app config directory: %s", err.Error())
}
env, err := LoadAppEnv(appName)
if err != nil {
return fmt.Errorf("Unable to load app environment: %s", err.Error())
}
oldEnvFile := filepath.Join(common.AppRoot(appName), "ENV")
return drainLegacyEnvFile(appName, oldEnvFile, env)
}
// drainLegacyEnvFile merges the legacy ENV file at oldEnvFile into env, records
// the migration against name, and removes the legacy file. The file is only
// removed once the merged environment has been written successfully, so a parse
// or write failure leaves the original in place.
//
// A legacy file that turns up after the migration has already been recorded was
// written by hand rather than through `dokku config:*`, so its contents are
// merged in and named in a warning rather than being discarded silently.
func drainLegacyEnvFile(name string, oldEnvFile string, env *Env) error {
migrated := common.PropertyGetDefault("config", name, envMigratedProperty, "") == "true"
if !common.FileExists(oldEnvFile) {
if migrated {
return nil
}
return writeEnvMigrated(name)
}
oldEnv, err := loadFromFile(name, oldEnvFile)
if err != nil {
return fmt.Errorf("Unable to load old environment: %s", err.Error())
}
if migrated && oldEnv.Len() > 0 {
common.LogWarn(fmt.Sprintf("Importing %s written outside of dokku config: %s", oldEnvFile, strings.Join(oldEnv.Keys(), " ")))
}
env.Merge(oldEnv)
if err := env.Write(); err != nil {
return fmt.Errorf("Unable to write environment: %s", err.Error())
}
if err := common.SetPermissions(common.SetPermissionInput{
Filename: env.Filename(),
Mode: os.FileMode(0600),
}); err != nil {
return fmt.Errorf("Unable to set permissions on environment: %s", err.Error())
}
if err := writeEnvMigrated(name); err != nil {
return err
}
if err := os.Remove(oldEnvFile); err != nil {
return fmt.Errorf("Unable to remove migrated file %s: %s", oldEnvFile, err.Error())
}
return nil
}
func writeEnvMigrated(name string) error {
if err := common.PropertyWrite("config", name, envMigratedProperty, "true"); err != nil {
return fmt.Errorf("Unable to set %s property: %s", envMigratedProperty, err.Error())
}
return nil
}

View File

@@ -0,0 +1,198 @@
package config
import (
"os"
"os/user"
"path/filepath"
"testing"
"github.com/dokku/dokku/plugins/common"
)
// setupMigrateEnv points the dokku env at temporary directories and tells the
// permission helpers to chown files to the current user (a no-op) so the test
// works without root. The package-level paths in config_test.go are captured at
// init against the real dokku directories, so these tests must not use them.
func setupMigrateEnv(t *testing.T) (dokkuRoot string, libRoot string) {
t.Helper()
libRoot = t.TempDir()
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)
return dokkuRoot, libRoot
}
func writeLegacyAppEnv(t *testing.T, dokkuRoot, appName, contents string) string {
t.Helper()
if err := os.MkdirAll(filepath.Join(dokkuRoot, appName), 0755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
path := filepath.Join(dokkuRoot, appName, "ENV")
if err := os.WriteFile(path, []byte(contents), 0600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
return path
}
func expectEnvValue(t *testing.T, appName, key, want string) {
t.Helper()
got, ok := Get(appName, key)
if !ok {
t.Fatalf("expected %s to be set for %s", key, appName)
}
if got != want {
t.Errorf("%s for %s = %q, want %q", key, appName, got, want)
}
}
func TestMigrateEnvFiles_DrainsAndRemovesAppFile(t *testing.T) {
dokkuRoot, _ := setupMigrateEnv(t)
legacy := writeLegacyAppEnv(t, dokkuRoot, "alpha", "export DOKKU_CHECKS_SKIPPED=worker\nexport MY_VAR=value\n")
if err := MigrateEnvFiles(); err != nil {
t.Fatalf("MigrateEnvFiles: %v", err)
}
expectEnvValue(t, "alpha", "DOKKU_CHECKS_SKIPPED", "worker")
expectEnvValue(t, "alpha", "MY_VAR", "value")
if _, err := os.Stat(legacy); !os.IsNotExist(err) {
t.Errorf("expected %s to be removed, got err=%v", legacy, err)
}
if common.PropertyGet("config", "alpha", envMigratedProperty) != "true" {
t.Errorf("expected %s property to be set for alpha", envMigratedProperty)
}
}
func TestMigrateEnvFiles_DrainsAndRemovesGlobalFile(t *testing.T) {
dokkuRoot, _ := setupMigrateEnv(t)
legacy := filepath.Join(dokkuRoot, "ENV")
if err := os.WriteFile(legacy, []byte("export DOKKU_WAIT_TO_RETIRE=30\n"), 0600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if err := MigrateEnvFiles(); err != nil {
t.Fatalf("MigrateEnvFiles: %v", err)
}
expectEnvValue(t, "--global", "DOKKU_WAIT_TO_RETIRE", "30")
if _, err := os.Stat(legacy); !os.IsNotExist(err) {
t.Errorf("expected %s to be removed, got err=%v", legacy, err)
}
if common.PropertyGet("config", "--global", envMigratedProperty) != "true" {
t.Errorf("expected %s property to be set globally", envMigratedProperty)
}
}
// TestMigrateEnvFiles_ImportsHandEditedFile covers a legacy file that turns up
// after the migration was already recorded, which means it was written by hand
// rather than through `dokku config:*`. Its contents are merged in rather than
// discarded.
func TestMigrateEnvFiles_ImportsHandEditedFile(t *testing.T) {
dokkuRoot, _ := setupMigrateEnv(t)
writeLegacyAppEnv(t, dokkuRoot, "alpha", "export FIRST=one\n")
if err := MigrateEnvFiles(); err != nil {
t.Fatalf("first MigrateEnvFiles: %v", err)
}
legacy := writeLegacyAppEnv(t, dokkuRoot, "alpha", "export SECOND=two\n")
if err := MigrateEnvFiles(); err != nil {
t.Fatalf("second MigrateEnvFiles: %v", err)
}
expectEnvValue(t, "alpha", "FIRST", "one")
expectEnvValue(t, "alpha", "SECOND", "two")
if _, err := os.Stat(legacy); !os.IsNotExist(err) {
t.Errorf("expected %s to be removed, got err=%v", legacy, err)
}
}
// TestMigrateEnvFiles_LegacyValueWins documents the merge precedence the
// hand-edit path depends on: the legacy file overwrites the value already held
// at the config path.
func TestMigrateEnvFiles_LegacyValueWins(t *testing.T) {
dokkuRoot, _ := setupMigrateEnv(t)
if err := os.MkdirAll(filepath.Join(dokkuRoot, "alpha"), 0755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := common.PropertySetupApp("config", "alpha"); err != nil {
t.Fatalf("PropertySetupApp: %v", err)
}
if err := setupAppConfigDir("alpha"); err != nil {
t.Fatalf("setupAppConfigDir: %v", err)
}
if err := SetMany("alpha", map[string]string{"SHARED": "new"}, false, false); err != nil {
t.Fatalf("SetMany: %v", err)
}
writeLegacyAppEnv(t, dokkuRoot, "alpha", "export SHARED=legacy\n")
if err := MigrateEnvFiles(); err != nil {
t.Fatalf("MigrateEnvFiles: %v", err)
}
expectEnvValue(t, "alpha", "SHARED", "legacy")
}
func TestMigrateEnvFiles_MarksAppsWithoutLegacyFile(t *testing.T) {
dokkuRoot, _ := setupMigrateEnv(t)
if err := os.MkdirAll(filepath.Join(dokkuRoot, "alpha"), 0755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := MigrateEnvFiles(); err != nil {
t.Fatalf("MigrateEnvFiles: %v", err)
}
if common.PropertyGet("config", "alpha", envMigratedProperty) != "true" {
t.Errorf("expected %s property to be set for an app with no legacy file", envMigratedProperty)
}
}
// TestMigrateEnvFiles_KeepsLegacyFileWhenWriteFails verifies the legacy file
// survives a failure to write the merged environment, so nothing is lost.
func TestMigrateEnvFiles_KeepsLegacyFileWhenWriteFails(t *testing.T) {
dokkuRoot, libRoot := setupMigrateEnv(t)
legacy := writeLegacyAppEnv(t, dokkuRoot, "alpha", "export MY_VAR=value\n")
// Occupy the app's config directory path with a regular file so
// MkdirAll cannot create it, even when the tests run as root.
if err := os.MkdirAll(filepath.Join(libRoot, "config"), 0755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join(libRoot, "config", "alpha"), []byte(""), 0600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if err := MigrateEnvFiles(); err == nil {
t.Fatalf("expected MigrateEnvFiles to fail")
}
if _, err := os.Stat(legacy); err != nil {
t.Errorf("expected %s to survive a failed migration, got err=%v", legacy, err)
}
if common.PropertyGet("config", "alpha", envMigratedProperty) == "true" {
t.Errorf("did not expect %s property to be set after a failed migration", envMigratedProperty)
}
}

View File

@@ -37,6 +37,8 @@ func main() {
case "config-get-global":
key := flag.Arg(0)
err = config.TriggerConfigGetGlobal(key)
case "config-migrate-env":
err = config.TriggerConfigMigrateEnv()
case "config-set":
appName := flag.Arg(0)
pairs := flag.Args()[1:]

View File

@@ -77,44 +77,11 @@ func setupAppConfigDir(appName string) error {
})
}
func migrateGlobalEnv() error {
if err := common.PropertySetup("--global"); err != nil {
return fmt.Errorf("Unable to setup global environment: %s", err.Error())
}
oldGlobalEnvFile := filepath.Join(common.MustGetEnv("DOKKU_ROOT"), "ENV")
isGlobalMigrated := common.PropertyGetDefault("config", "--global", "env-migrated", "")
if isGlobalMigrated == "true" {
return nil
}
oldGlobalEnv, err := loadFromFile("--global", oldGlobalEnvFile)
if err != nil {
return fmt.Errorf("Unable to load old global environment: %s", err.Error())
}
globalEnv, err := LoadGlobalEnv()
if err != nil {
return fmt.Errorf("Unable to load global environment: %s", err.Error())
}
globalEnv.Merge(oldGlobalEnv)
if err := globalEnv.Write(); err != nil {
return fmt.Errorf("Unable to write global environment: %s", err.Error())
}
if err := common.SetPermissions(common.SetPermissionInput{
Filename: globalEnv.Filename(),
Mode: os.FileMode(0600),
}); err != nil {
return fmt.Errorf("Unable to set permissions on global environment: %s", err.Error())
}
if err := common.PropertyWrite("config", "--global", "env-migrated", "true"); err != nil {
return fmt.Errorf("Unable to set env-migrated property: %s", err.Error())
}
return nil
// TriggerConfigMigrateEnv drains the pre-0.38 ENV files into the config
// property path. Exposed as a trigger so plugins that install before config can
// force the migration before reading a deprecated config var.
func TriggerConfigMigrateEnv() error {
return MigrateEnvFiles()
}
// TriggerInstall runs the install step for the config plugin
@@ -123,65 +90,7 @@ func TriggerInstall() error {
return fmt.Errorf("Unable to install the config plugin: %s", err.Error())
}
if err := migrateGlobalEnv(); err != nil {
return fmt.Errorf("Unable to migrate global environment: %s", err.Error())
}
apps, err := common.UnfilteredDokkuApps()
if err != nil {
return nil
}
// migrate all app ENV files to config path
for _, appName := range apps {
if err := common.PropertySetupApp("config", appName); err != nil {
return fmt.Errorf("Unable to setup app environment: %s", err.Error())
}
if err := setupAppConfigDir(appName); err != nil {
return fmt.Errorf("Unable to setup app config directory: %s", err.Error())
}
oldEnvFile := filepath.Join(common.AppRoot(appName), "ENV")
isMigrated := common.PropertyGetDefault("config", appName, "env-migrated", "")
// delete the old file on the next install
if isMigrated == "true" {
if err := os.RemoveAll(oldEnvFile); err != nil {
return fmt.Errorf("Unable to remove old ENV file: %s", err.Error())
}
continue
}
// skip if the file doesn't exist
if _, err := os.Stat(oldEnvFile); err != nil {
if err := common.PropertyWrite("config", appName, "env-migrated", "true"); err != nil {
return fmt.Errorf("Unable to set env-migrated property: %s", err.Error())
}
continue
}
// merge in the old env into the new env
oldEnv, err := loadFromFile(appName, oldEnvFile)
if err != nil {
return fmt.Errorf("Unable to load old environment: %s", err.Error())
}
env, err := LoadAppEnv(appName)
if err != nil {
return fmt.Errorf("Unable to load app environment: %s", err.Error())
}
env.Merge(oldEnv)
if err := env.Write(); err != nil {
return fmt.Errorf("Unable to write app environment: %s", err.Error())
}
if err := common.PropertyWrite("config", appName, "env-migrated", "true"); err != nil {
return fmt.Errorf("Unable to set env-migrated property: %s", err.Error())
}
}
return nil
return MigrateEnvFiles()
}
// TriggerPostAppCloneSetup creates new buildpacks files

View File

@@ -586,3 +586,69 @@ teardown() {
echo "status: $status"
assert_output '[{"name":"BKEY","value":"true"},{"name":"aKey","value":"true"},{"name":"bKey","value":"true"},{"name":"zKey","value":"true"}]'
}
@test "(config) install migrates a pre-0.38 ENV file before plugins read it" {
# The checks plugin installs before the config plugin, so before this was
# fixed it read the not-yet-relocated ENV file, found nothing, and migrated
# DOKKU_CHECKS_SKIPPED to the checks property silently doing nothing.
run /bin/bash -c "sudo rm -f ${DOKKU_LIB_ROOT}/config/config/$TEST_APP/env-migrated ${DOKKU_LIB_ROOT}/config/checks/$TEST_APP/skipped"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "echo 'export DOKKU_CHECKS_SKIPPED=worker' | sudo tee $DOKKU_ROOT/$TEST_APP/ENV && sudo chown dokku:dokku $DOKKU_ROOT/$TEST_APP/ENV"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku plugin:install --core"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku checks:report $TEST_APP --checks-skipped-list"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "worker"
run /bin/bash -c "dokku config:show $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "DOKKU_CHECKS_SKIPPED" 0
run /bin/bash -c "test -f $DOKKU_ROOT/$TEST_APP/ENV"
echo "status: $status"
assert_failure
}
@test "(config) config-migrate-env imports an ENV file written after migration" {
# drain once so the migration is on record, which makes the ENV file staged
# below one that could only have been written by hand
run_plugn_trigger config-migrate-env
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "echo 'export HAND_EDITED=yes' | sudo tee $DOKKU_ROOT/$TEST_APP/ENV && sudo chown dokku:dokku $DOKKU_ROOT/$TEST_APP/ENV"
echo "output: $output"
echo "status: $status"
assert_success
run_plugn_trigger config-migrate-env
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "$DOKKU_ROOT/$TEST_APP/ENV"
run /bin/bash -c "dokku config:get $TEST_APP HAND_EDITED"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "yes"
run /bin/bash -c "test -f $DOKKU_ROOT/$TEST_APP/ENV"
echo "status: $status"
assert_failure
}