feat: scope docker-options to specific procfile processes

Adds a `--process` flag (repeatable) to docker-options:add/remove/clear/list and the new docker-options:list subcommand for querying a single process+phase pair. Process scoping is supported only for the deploy phase since build runs once per app and run covers ad-hoc commands and cron tasks where no Procfile process type is available. Storage moves from `$DOKKU_ROOT/$APP/DOCKER_OPTIONS_*` files to property lists under `/var/lib/dokku/config/docker-options/$APP/{processType}.{phase}`, with `_default_` as the sentinel for app-wide options. The install trigger migrates pre-existing DOCKER_OPTIONS_* files into property lists once and renames them to `.migrated`; a global marker makes re-runs strictly no-op. The legacy docker-args-{build,deploy,run} bash triggers are reimplemented in Go alongside a new docker-args-process-deploy trigger that surfaces per-process options to the scheduler. The :report command exposes one dynamic flag per configured `process.deploy` pair (e.g. `--docker-options-deploy.web`) and supports `--format json`. There is no `--global` flag; omitting `--process` keeps the historical default behaviour, since `--global` elsewhere in dokku means "across all apps". Closes #2441.
This commit is contained in:
Jose Diaz-Gonzalez
2026-04-27 18:12:52 -04:00
parent f19227dcd9
commit 9c8d5f54fb
16 changed files with 1034 additions and 385 deletions

View File

@@ -20,14 +20,9 @@ func main() {
case "add":
args := flag.NewFlagSet("docker-options:add", flag.ExitOnError)
args.SetInterspersed(false)
processes := args.StringSlice("process", []string{}, "process types to scope this option to (reserved for future use)")
global := args.Bool("global", false, "explicitly mark as a global option (reserved for future use)")
processes := args.StringSlice("process", []string{}, "process types to scope this option to (deploy phase only)")
args.Parse(os.Args[2:])
if err = dockeroptions.ErrIfReservedFlagsUsed(*processes, *global); err != nil {
break
}
positional := args.Args()
appName := ""
phases := ""
@@ -41,18 +36,13 @@ func main() {
if len(positional) > 2 {
optionParts = positional[2:]
}
err = dockeroptions.CommandAdd(appName, phases, strings.Join(optionParts, " "))
err = dockeroptions.CommandAdd(appName, *processes, phases, strings.Join(optionParts, " "))
case "remove":
args := flag.NewFlagSet("docker-options:remove", flag.ExitOnError)
args.SetInterspersed(false)
processes := args.StringSlice("process", []string{}, "process types to scope this option to (reserved for future use)")
global := args.Bool("global", false, "explicitly mark as a global option (reserved for future use)")
processes := args.StringSlice("process", []string{}, "process types to scope this option to (deploy phase only)")
args.Parse(os.Args[2:])
if err = dockeroptions.ErrIfReservedFlagsUsed(*processes, *global); err != nil {
break
}
positional := args.Args()
appName := ""
phases := ""
@@ -66,18 +56,13 @@ func main() {
if len(positional) > 2 {
optionParts = positional[2:]
}
err = dockeroptions.CommandRemove(appName, phases, strings.Join(optionParts, " "))
err = dockeroptions.CommandRemove(appName, *processes, phases, strings.Join(optionParts, " "))
case "clear":
args := flag.NewFlagSet("docker-options:clear", flag.ExitOnError)
args.SetInterspersed(false)
processes := args.StringSlice("process", []string{}, "process types to scope this option to (reserved for future use)")
global := args.Bool("global", false, "explicitly mark as a global option (reserved for future use)")
processes := args.StringSlice("process", []string{}, "process types to scope this option to (deploy phase only)")
args.Parse(os.Args[2:])
if err = dockeroptions.ErrIfReservedFlagsUsed(*processes, *global); err != nil {
break
}
positional := args.Args()
appName := ""
phases := ""
@@ -87,7 +72,16 @@ func main() {
if len(positional) > 1 {
phases = positional[1]
}
err = dockeroptions.CommandClear(appName, phases)
err = dockeroptions.CommandClear(appName, *processes, phases)
case "list":
args := flag.NewFlagSet("docker-options:list", flag.ExitOnError)
args.SetInterspersed(false)
processType := args.String("process", "", "process type to query (omit for the default scope)")
phase := args.String("phase", "", "phase to query [build|deploy|run] (required)")
args.Parse(os.Args[2:])
appName := args.Arg(0)
err = dockeroptions.CommandList(appName, *processType, *phase)
case "report":
args := flag.NewFlagSet("docker-options:report", flag.ExitOnError)
format := args.String("format", "stdout", "format: [ stdout | json ]")

View File

@@ -17,6 +17,25 @@ func main() {
var err error
switch trigger {
case "install":
err = dockeroptions.TriggerInstall()
case "docker-args-build":
appName := flag.Arg(0)
imageSourceType := flag.Arg(1)
err = dockeroptions.TriggerDockerArgs("build", appName, imageSourceType, "")
case "docker-args-deploy":
appName := flag.Arg(0)
imageSourceType := flag.Arg(1)
err = dockeroptions.TriggerDockerArgs("deploy", appName, imageSourceType, "")
case "docker-args-run":
appName := flag.Arg(0)
imageSourceType := flag.Arg(1)
err = dockeroptions.TriggerDockerArgs("run", appName, imageSourceType, "")
case "docker-args-process-deploy":
appName := flag.Arg(0)
imageSourceType := flag.Arg(1)
processType := flag.Arg(3)
err = dockeroptions.TriggerDockerArgsProcessDeploy(appName, imageSourceType, processType)
case "post-app-clone-setup":
oldAppName := flag.Arg(0)
newAppName := flag.Arg(1)