From 4eb42f1b82d134fa195597c5b59360b04eddfd1d Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 27 Apr 2026 14:11:37 -0400 Subject: [PATCH 1/3] refactor: migrate docker-options subcommands to go Ports the user-facing CLI subcommands and lifecycle triggers from bash to go, mirroring the layout used by the ports plugin. The on-disk format (`DOCKER_OPTIONS_BUILD/DEPLOY/RUN` files), the `docker-args-build/deploy/run` triggers, and every exported helper consumed by other plugins are unchanged. Reserves the `--process` and `--global` flags on add, remove, and clear so usage today exits with a not-yet-implemented error and the surface is locked in ahead of the process-scoping work tracked in #2441. --- plugins/docker-options/Makefile | 6 + plugins/docker-options/commands | 16 --- plugins/docker-options/functions | 112 ------------------ plugins/docker-options/go.mod | 5 +- plugins/docker-options/go.sum | 2 + plugins/docker-options/help-functions | 35 ------ plugins/docker-options/internal-functions | 75 ------------ plugins/docker-options/internal-functions.go | 45 +++++++ plugins/docker-options/post-app-clone-setup | 20 ---- plugins/docker-options/post-app-rename-setup | 20 ---- plugins/docker-options/post-delete | 14 --- plugins/docker-options/report | 6 - plugins/docker-options/report.go | 50 ++++++++ .../docker-options/src/commands/commands.go | 57 +++++++++ .../src/subcommands/subcommands.go | 109 +++++++++++++++++ .../docker-options/src/triggers/triggers.go | 41 +++++++ plugins/docker-options/subcommands.go | 105 ++++++++++++++++ plugins/docker-options/subcommands/add | 21 ---- plugins/docker-options/subcommands/clear | 28 ----- plugins/docker-options/subcommands/default | 6 - plugins/docker-options/subcommands/remove | 20 ---- plugins/docker-options/subcommands/report | 6 - plugins/docker-options/triggers.go | 65 ++++++++++ 23 files changed, 484 insertions(+), 380 deletions(-) create mode 100644 plugins/docker-options/Makefile delete mode 100755 plugins/docker-options/commands delete mode 100755 plugins/docker-options/functions delete mode 100755 plugins/docker-options/help-functions delete mode 100755 plugins/docker-options/internal-functions create mode 100644 plugins/docker-options/internal-functions.go delete mode 100755 plugins/docker-options/post-app-clone-setup delete mode 100755 plugins/docker-options/post-app-rename-setup delete mode 100755 plugins/docker-options/post-delete delete mode 100755 plugins/docker-options/report create mode 100644 plugins/docker-options/report.go create mode 100644 plugins/docker-options/src/commands/commands.go create mode 100644 plugins/docker-options/src/subcommands/subcommands.go create mode 100644 plugins/docker-options/src/triggers/triggers.go create mode 100644 plugins/docker-options/subcommands.go delete mode 100755 plugins/docker-options/subcommands/add delete mode 100755 plugins/docker-options/subcommands/clear delete mode 100755 plugins/docker-options/subcommands/default delete mode 100755 plugins/docker-options/subcommands/remove delete mode 100755 plugins/docker-options/subcommands/report create mode 100644 plugins/docker-options/triggers.go diff --git a/plugins/docker-options/Makefile b/plugins/docker-options/Makefile new file mode 100644 index 000000000..0e9019a9f --- /dev/null +++ b/plugins/docker-options/Makefile @@ -0,0 +1,6 @@ +SUBCOMMANDS = subcommands/add subcommands/clear subcommands/remove subcommands/report +TRIGGERS = triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-delete triggers/report +BUILD = commands subcommands triggers +PLUGIN_NAME = docker-options + +include ../../common.mk diff --git a/plugins/docker-options/commands b/plugins/docker-options/commands deleted file mode 100755 index f404d4a31..000000000 --- a/plugins/docker-options/commands +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -[[ " help docker-options:help " == *" $1 "* ]] || exit "$DOKKU_NOT_IMPLEMENTED_EXIT" -source "$PLUGIN_AVAILABLE_PATH/docker-options/help-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -case "$1" in - help | docker-options:help) - cmd-docker-options-help "$@" - ;; - - *) - exit "$DOKKU_NOT_IMPLEMENTED_EXIT" - ;; - -esac diff --git a/plugins/docker-options/functions b/plugins/docker-options/functions deleted file mode 100755 index e28e78f47..000000000 --- a/plugins/docker-options/functions +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" - -AVAILABLE_PHASES=(build deploy run) - -fn-get-phase-file-path() { - declare desc="return docker-options config file path for specified phase" - local APP="$1" PHASE="$2" - local PHASE_FILE="${DOKKU_ROOT}/${APP}/DOCKER_OPTIONS_${PHASE^^}" - echo "$PHASE_FILE" -} - -get_phase_file_path() { - declare desc="return docker-options config file path for specified phase" - local phase_file_prefix="DOCKER_OPTIONS_" - local phase=$1 - [[ "$APP" && "$phase" ]] || dokku_log_fail "Error: phase_file_path is incomplete." - fn-get-phase-file-path "$APP" "$phase" -} - -get_phases() { - declare desc="returns array of passed passes if all are in allowed array" - local passed_phases_list="$1" - # shellcheck disable=SC2001 - local -r phases_allowed=$(sed -e 's/ /\|/g' <<<"${AVAILABLE_PHASES[@]}") - local phase - local passed_phases - if [[ -n "$passed_phases_list" ]]; then - OIFS=$IFS - IFS=',' - read -ra passed_phases <<<"$passed_phases_list" - IFS=$OIFS - for phase in "${passed_phases[@]}"; do - [[ "$phase" == @($phases_allowed) ]] || dokku_log_fail "Phase(s) must be one of [${AVAILABLE_PHASES[*]}]" - done - echo "${passed_phases[@]}" - fi -} - -create_phase_file_if_required() { - declare desc="create phase file" - local phase_file_path="$1" - [[ -f "$phase_file_path" ]] || { - touch "$phase_file_path" - } -} - -display_phase_options() { - declare desc="print docker-options for specified phase" - local phase="$1" - local phase_file_path="$2" - echo "${phase^} options:" - sed -e 's/^/ /' "$phase_file_path" -} - -display_all_phases_options() { - declare desc="print docker-options for all phases" - local phases=("${AVAILABLE_PHASES[@]}") - for phase in "${phases[@]}"; do - local phase_file_path="$(get_phase_file_path "$phase")" - if [[ -s "$phase_file_path" ]]; then - display_phase_options "$phase" "$phase_file_path" - fi - done -} - -display_passed_phases_options() { - declare desc="print docker options for list of specified phases" - local passed_phases=("${!1}") - local phase - for phase in "${passed_phases[@]}"; do - local phase_file_path="$(get_phase_file_path "$phase")" - if [[ ! -s "$phase_file_path" ]]; then - echo "${phase^} options: none" - else - display_phase_options "$phase" "$phase_file_path" - fi - done -} - -add_passed_docker_option() { - declare desc="adds docker option to specified phases" - local passed_phases=("${!1}") - shift - local passed_option_string="$*" - local phase - for phase in "${passed_phases[@]}"; do - local phase_file_path="$(get_phase_file_path "$phase")" - create_phase_file_if_required "$phase_file_path" - echo "${passed_option_string}" >>"$phase_file_path" - local all_phase_options="$(<"$phase_file_path")" - echo -e "${all_phase_options}" | sed '/^$/d' | sort -u >"$phase_file_path" - done -} - -remove_passed_docker_option() { - declare desc="removes docker option from specified phases" - local passed_phases=("${!1}") - shift - local passed_option_string="$*" - local phase - for phase in "${passed_phases[@]}"; do - local phase_file_path="$(get_phase_file_path "$phase")" - [[ ! -s "$phase_file_path" ]] || { - local all_phase_options="$(<"$phase_file_path")" - local all_phase_options=$(echo -e "${all_phase_options}" | sed "s#^${passed_option_string}\$##") - echo -e "${all_phase_options}" | sed '/^$/d' | sort -u >"$phase_file_path" - } - done -} diff --git a/plugins/docker-options/go.mod b/plugins/docker-options/go.mod index cc765fc5b..b76b55128 100644 --- a/plugins/docker-options/go.mod +++ b/plugins/docker-options/go.mod @@ -2,7 +2,10 @@ module github.com/dokku/dokku/plugins/docker-options go 1.26.2 -require github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000 +require ( + github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000 + github.com/spf13/pflag v1.0.10 +) require ( github.com/alexellis/go-execute/v2 v2.2.1 // indirect diff --git a/plugins/docker-options/go.sum b/plugins/docker-options/go.sum index d7b54471f..c97fc9665 100644 --- a/plugins/docker-options/go.sum +++ b/plugins/docker-options/go.sum @@ -32,6 +32,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= diff --git a/plugins/docker-options/help-functions b/plugins/docker-options/help-functions deleted file mode 100755 index 71d8c427a..000000000 --- a/plugins/docker-options/help-functions +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-docker-options-help() { - declare desc="help command" - declare CMD="$1" - local plugin_name="docker-options" - local plugin_description="Manage docker options for an app" - - if [[ "$CMD" == "${plugin_name}:help" ]]; then - echo -e "Usage: dokku ${plugin_name}[:COMMAND]" - echo '' - echo "$plugin_description" - echo '' - echo 'Additional commands:' - fn-help-content | sort | column -c2 -t -s, - elif [[ $(ps -o command= $PPID) == *"--all"* ]]; then - fn-help-content - else - cat < OPTION, Add Docker option to app for phase (comma separated phase list) - docker-options:clear [phase(s)], Clear a docker options from application with an optional phase (comma separated phase list) - docker-options:remove OPTION, Remove Docker option from app for phase (comma separated phase list) - docker-options:report [] [], Displays a docker options report for one or more apps -help_content -} diff --git a/plugins/docker-options/internal-functions b/plugins/docker-options/internal-functions deleted file mode 100755 index 5634f4f28..000000000 --- a/plugins/docker-options/internal-functions +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/docker-options/functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-docker-options-report() { - declare desc="displays a docker options report for one or more apps" - declare cmd="docker-options:report" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" INFO_FLAG="$2" - - if [[ -n "$APP" ]] && [[ "$APP" == --* ]]; then - INFO_FLAG="$APP" - APP="" - fi - - if [[ -z "$APP" ]] && [[ -z "$INFO_FLAG" ]]; then - INFO_FLAG="true" - fi - - if [[ -z "$APP" ]]; then - for app in $(dokku_apps); do - cmd-docker-options-report-single "$app" "$INFO_FLAG" | tee || true - done - else - cmd-docker-options-report-single "$APP" "$INFO_FLAG" - fi -} - -cmd-docker-options-report-single() { - declare APP="$1" INFO_FLAG="$2" - if [[ "$INFO_FLAG" == "true" ]]; then - INFO_FLAG="" - fi - verify_app_name "$APP" - local flag_map=( - "--docker-options-build: $(fn-docker-options "$APP" build)" - "--docker-options-deploy: $(fn-docker-options "$APP" deploy)" - "--docker-options-run: $(fn-docker-options "$APP" run)" - ) - - if [[ -z "$INFO_FLAG" ]]; then - dokku_log_info2_quiet "$APP docker options information" - for flag in "${flag_map[@]}"; do - key="$(echo "${flag#--}" | cut -f1 -d' ' | tr - ' ')" - dokku_log_verbose "$(printf "%-30s %-25s" "${key^}" "${flag#*: }")" - done - else - local match=false - local value_exists=false - for flag in "${flag_map[@]}"; do - valid_flags="${valid_flags} $(echo "$flag" | cut -d':' -f1)" - if [[ "$flag" == "${INFO_FLAG}:"* ]]; then - value=${flag#*: } - size="${#value}" - if [[ "$size" -ne 0 ]]; then - echo "$value" && match=true && value_exists=true - else - match=true - fi - fi - done - [[ "$match" == "true" ]] || dokku_log_fail "Invalid flag passed, valid flags:${valid_flags}" - echo "" - fi -} - -fn-docker-options() { - declare APP="$1" PHASE="$2" - local PHASE_FILE="$(fn-get-phase-file-path "$APP" "$PHASE")" - if [[ -r "$PHASE_FILE" ]]; then - tr '\n' ' ' <"$PHASE_FILE" - fi -} diff --git a/plugins/docker-options/internal-functions.go b/plugins/docker-options/internal-functions.go new file mode 100644 index 000000000..70cb95513 --- /dev/null +++ b/plugins/docker-options/internal-functions.go @@ -0,0 +1,45 @@ +package dockeroptions + +import ( + "errors" + "fmt" + "strings" +) + +var availablePhases = []string{"build", "deploy", "run"} + +func parsePhases(phasesArg string) ([]string, error) { + if phasesArg == "" { + return nil, nil + } + + phases := strings.Split(phasesArg, ",") + for _, phase := range phases { + valid := false + for _, allowed := range availablePhases { + if phase == allowed { + valid = true + break + } + } + if !valid { + return nil, fmt.Errorf("Phase(s) must be one of [%s]", strings.Join(availablePhases, " ")) + } + } + + return phases, nil +} + +// ErrIfReservedFlagsUsed returns a not-yet-implemented error when --process or --global are passed. +// Both flags are reserved for the upcoming process-scoped docker options work (issue #2441). +func ErrIfReservedFlagsUsed(processes []string, global bool) error { + if len(processes) == 0 && !global { + return nil + } + + if len(processes) > 0 && global { + return errors.New("--process and --global flags are mutually exclusive") + } + + return errors.New("--process and --global flags are reserved for a future release and not yet implemented") +} diff --git a/plugins/docker-options/post-app-clone-setup b/plugins/docker-options/post-app-clone-setup deleted file mode 100755 index 9b36b329d..000000000 --- a/plugins/docker-options/post-app-clone-setup +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_AVAILABLE_PATH/docker-options/functions" - -trigger-docker-options-post-app-clone-setup() { - declare desc="copies docker options over" - declare trigger="post-app-clone-setup" - declare OLD_APP="$1" NEW_APP="$2" - - declare -a phases - phases=("build" "deploy" "run") - for phase in "${phases[@]}"; do - if [[ -f "$(fn-get-phase-file-path "$OLD_APP" "$phase")" ]]; then - cp "$(fn-get-phase-file-path "$OLD_APP" "$phase")" "$(fn-get-phase-file-path "$NEW_APP" "$phase")" - fi - done -} - -trigger-docker-options-post-app-clone-setup "$@" diff --git a/plugins/docker-options/post-app-rename-setup b/plugins/docker-options/post-app-rename-setup deleted file mode 100755 index 0688252a6..000000000 --- a/plugins/docker-options/post-app-rename-setup +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_AVAILABLE_PATH/docker-options/functions" - -trigger-docker-options-post-app-rename-setup() { - declare desc="copies docker options over" - declare trigger="post-app-rename-setup" - declare OLD_APP="$1" NEW_APP="$2" - - declare -a phases - phases=("build" "deploy" "run") - for phase in "${phases[@]}"; do - if [[ -f "$(fn-get-phase-file-path "$OLD_APP" "$phase")" ]]; then - cp "$(fn-get-phase-file-path "$OLD_APP" "$phase")" "$(fn-get-phase-file-path "$NEW_APP" "$phase")" - fi - done -} - -trigger-docker-options-post-app-rename-setup "$@" diff --git a/plugins/docker-options/post-delete b/plugins/docker-options/post-delete deleted file mode 100755 index 3505fb6a8..000000000 --- a/plugins/docker-options/post-delete +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_AVAILABLE_PATH/docker-options/functions" - -trigger-docker-options-post-delete() { - declare desc="deletes docker options files for an app" - declare trigger="post-delete" - declare APP="$1" - - rm -f "$(fn-get-phase-file-path "$APP" "build")" "$(fn-get-phase-file-path "$APP" "deploy")" "$(fn-get-phase-file-path "$APP" "run")" -} - -trigger-docker-options-post-delete "$@" diff --git a/plugins/docker-options/report b/plugins/docker-options/report deleted file mode 100755 index 722271960..000000000 --- a/plugins/docker-options/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/docker-options/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-docker-options-report-single "$@" diff --git a/plugins/docker-options/report.go b/plugins/docker-options/report.go new file mode 100644 index 000000000..20c6117e0 --- /dev/null +++ b/plugins/docker-options/report.go @@ -0,0 +1,50 @@ +package dockeroptions + +import ( + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +// ReportSingleApp displays the docker options report for a single app +func ReportSingleApp(appName string, format string, infoFlag string) error { + if err := common.VerifyAppName(appName); err != nil { + return err + } + + flags := map[string]common.ReportFunc{ + "--docker-options-build": reportBuildOptions, + "--docker-options-deploy": reportDeployOptions, + "--docker-options-run": reportRunOptions, + } + + flagKeys := []string{} + for flagKey := range flags { + flagKeys = append(flagKeys, flagKey) + } + + trimPrefix := false + uppercaseFirstCharacter := true + infoFlags := common.CollectReport(appName, infoFlag, flags) + return common.ReportSingleApp("docker options", appName, infoFlag, infoFlags, flagKeys, format, trimPrefix, uppercaseFirstCharacter) +} + +func reportBuildOptions(appName string) string { + return joinPhaseOptions(appName, "build") +} + +func reportDeployOptions(appName string) string { + return joinPhaseOptions(appName, "deploy") +} + +func reportRunOptions(appName string) string { + return joinPhaseOptions(appName, "run") +} + +func joinPhaseOptions(appName string, phase string) string { + options, err := GetDockerOptionsForPhase(appName, phase) + if err != nil || len(options) == 0 { + return "" + } + return strings.Join(options, " ") +} diff --git a/plugins/docker-options/src/commands/commands.go b/plugins/docker-options/src/commands/commands.go new file mode 100644 index 000000000..c8c85c6bb --- /dev/null +++ b/plugins/docker-options/src/commands/commands.go @@ -0,0 +1,57 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strconv" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +const ( + helpHeader = `Usage: dokku docker-options[:COMMAND] + +Manage docker options for an app + +Additional commands:` + + helpContent = ` + docker-options:add OPTION, Add Docker option to app for phase (comma separated phase list) + docker-options:clear [phase(s)], Clear a docker options from application with an optional phase (comma separated phase list) + docker-options:remove OPTION, Remove Docker option from app for phase (comma separated phase list) + docker-options:report [] [], Displays a docker options report for one or more apps` +) + +func main() { + flag.Usage = usage + flag.Parse() + + cmd := flag.Arg(0) + switch cmd { + case "docker-options", "docker-options:help": + usage() + case "help": + result, err := common.CallExecCommand(common.ExecCommandInput{ + Command: "ps", + Args: []string{"-o", "command=", strconv.Itoa(os.Getppid())}, + }) + if err == nil && strings.Contains(result.StdoutContents(), "--all") { + fmt.Println(helpContent) + } else { + fmt.Print("\n docker-options, Manage docker options for an app\n") + } + default: + dokkuNotImplementExitCode, err := strconv.Atoi(os.Getenv("DOKKU_NOT_IMPLEMENTED_EXIT")) + if err != nil { + fmt.Println("failed to retrieve DOKKU_NOT_IMPLEMENTED_EXIT environment variable") + dokkuNotImplementExitCode = 10 + } + os.Exit(dokkuNotImplementExitCode) + } +} + +func usage() { + common.CommandUsage(helpHeader, helpContent) +} diff --git a/plugins/docker-options/src/subcommands/subcommands.go b/plugins/docker-options/src/subcommands/subcommands.go new file mode 100644 index 000000000..d70e97263 --- /dev/null +++ b/plugins/docker-options/src/subcommands/subcommands.go @@ -0,0 +1,109 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" + dockeroptions "github.com/dokku/dokku/plugins/docker-options" + + flag "github.com/spf13/pflag" +) + +func main() { + parts := strings.Split(os.Args[0], "/") + subcommand := parts[len(parts)-1] + + var err error + switch subcommand { + 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)") + args.Parse(os.Args[2:]) + + if err = dockeroptions.ErrIfReservedFlagsUsed(*processes, *global); err != nil { + break + } + + positional := args.Args() + appName := "" + phases := "" + var optionParts []string + if len(positional) > 0 { + appName = positional[0] + } + if len(positional) > 1 { + phases = positional[1] + } + if len(positional) > 2 { + optionParts = positional[2:] + } + err = dockeroptions.CommandAdd(appName, 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)") + args.Parse(os.Args[2:]) + + if err = dockeroptions.ErrIfReservedFlagsUsed(*processes, *global); err != nil { + break + } + + positional := args.Args() + appName := "" + phases := "" + var optionParts []string + if len(positional) > 0 { + appName = positional[0] + } + if len(positional) > 1 { + phases = positional[1] + } + if len(positional) > 2 { + optionParts = positional[2:] + } + err = dockeroptions.CommandRemove(appName, 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)") + args.Parse(os.Args[2:]) + + if err = dockeroptions.ErrIfReservedFlagsUsed(*processes, *global); err != nil { + break + } + + positional := args.Args() + appName := "" + phases := "" + if len(positional) > 0 { + appName = positional[0] + } + if len(positional) > 1 { + phases = positional[1] + } + err = dockeroptions.CommandClear(appName, phases) + case "report": + args := flag.NewFlagSet("docker-options:report", flag.ExitOnError) + format := args.String("format", "stdout", "format: [ stdout | json ]") + osArgs, infoFlag, flagErr := common.ParseReportArgs("docker-options", os.Args[2:]) + if flagErr == nil { + args.Parse(osArgs) + appName := args.Arg(0) + err = dockeroptions.CommandReport(appName, *format, infoFlag) + } else { + err = flagErr + } + default: + err = fmt.Errorf("Invalid plugin subcommand call: %s", subcommand) + } + + if err != nil { + common.LogFailWithError(err) + } +} diff --git a/plugins/docker-options/src/triggers/triggers.go b/plugins/docker-options/src/triggers/triggers.go new file mode 100644 index 000000000..f65ff8554 --- /dev/null +++ b/plugins/docker-options/src/triggers/triggers.go @@ -0,0 +1,41 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" + dockeroptions "github.com/dokku/dokku/plugins/docker-options" +) + +func main() { + parts := strings.Split(os.Args[0], "/") + trigger := parts[len(parts)-1] + flag.Parse() + + var err error + switch trigger { + case "post-app-clone-setup": + oldAppName := flag.Arg(0) + newAppName := flag.Arg(1) + err = dockeroptions.TriggerPostAppCloneSetup(oldAppName, newAppName) + case "post-app-rename-setup": + oldAppName := flag.Arg(0) + newAppName := flag.Arg(1) + err = dockeroptions.TriggerPostAppRenameSetup(oldAppName, newAppName) + case "post-delete": + appName := flag.Arg(0) + err = dockeroptions.TriggerPostDelete(appName) + case "report": + appName := flag.Arg(0) + err = dockeroptions.ReportSingleApp(appName, "", "") + default: + err = fmt.Errorf("Invalid plugin trigger call: %s", trigger) + } + + if err != nil { + common.LogFailWithError(err) + } +} diff --git a/plugins/docker-options/subcommands.go b/plugins/docker-options/subcommands.go new file mode 100644 index 000000000..f68606245 --- /dev/null +++ b/plugins/docker-options/subcommands.go @@ -0,0 +1,105 @@ +package dockeroptions + +import ( + "errors" + "fmt" + "os" + + "github.com/dokku/dokku/plugins/common" +) + +// CommandAdd adds a docker option to the specified phases for an app +func CommandAdd(appName string, phasesArg string, option string) error { + if err := common.VerifyAppName(appName); err != nil { + return err + } + + phases, err := parsePhases(phasesArg) + if err != nil { + return err + } + + if option == "" { + return errors.New("Please specify docker options to add to the phase") + } + + return AddDockerOptionToPhases(appName, phases, option) +} + +// CommandRemove removes a docker option from the specified phases for an app +func CommandRemove(appName string, phasesArg string, option string) error { + if err := common.VerifyAppName(appName); err != nil { + return err + } + + phases, err := parsePhases(phasesArg) + if err != nil { + return err + } + + if option == "" { + return errors.New("Please specify docker options to remove from the phase") + } + + return RemoveDockerOptionFromPhases(appName, phases, option) +} + +// CommandClear removes all docker options for an app, optionally limited to a list of phases +func CommandClear(appName string, phasesArg string) error { + if err := common.VerifyAppName(appName); err != nil { + return err + } + + if phasesArg == "" { + common.LogInfo1(fmt.Sprintf("Clearing docker-options for %s on all phases", appName)) + for _, phase := range availablePhases { + if err := removePhaseFile(appName, phase); err != nil { + return err + } + } + return nil + } + + phases, err := parsePhases(phasesArg) + if err != nil { + return err + } + + for _, phase := range phases { + common.LogInfo1(fmt.Sprintf("Clearing docker-options for %s on phase %s", appName, phase)) + if err := removePhaseFile(appName, phase); err != nil { + return err + } + } + + return nil +} + +// CommandReport displays a docker-options report for one or more apps +func CommandReport(appName string, format string, infoFlag string) error { + if appName == "" { + apps, err := common.DokkuApps() + if err != nil { + if errors.Is(err, common.NoAppsExist) { + common.LogWarn(err.Error()) + return nil + } + return err + } + for _, name := range apps { + if err := ReportSingleApp(name, format, infoFlag); err != nil { + return err + } + } + return nil + } + + return ReportSingleApp(appName, format, infoFlag) +} + +func removePhaseFile(appName string, phase string) error { + if err := os.Remove(getPhaseFilePath(appName, phase)); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} diff --git a/plugins/docker-options/subcommands/add b/plugins/docker-options/subcommands/add deleted file mode 100755 index 98233ad1a..000000000 --- a/plugins/docker-options/subcommands/add +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/docker-options/functions" - -cmd-docker-options-add() { - declare desc="Add a docker option to application" - declare cmd="docker-options:add" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - - verify_app_name "$APP" - read -ra passed_phases <<<"$(get_phases "$2")" - shift 2 # everything else passed is the docker option - local passed_docker_option="$*" - [[ -z "$passed_docker_option" ]] && dokku_log_fail "Please specify docker options to add to the phase" - add_passed_docker_option passed_phases[@] "${passed_docker_option[@]}" -} - -cmd-docker-options-add "$@" diff --git a/plugins/docker-options/subcommands/clear b/plugins/docker-options/subcommands/clear deleted file mode 100755 index 83c0501dd..000000000 --- a/plugins/docker-options/subcommands/clear +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/docker-options/functions" - -cmd-docker-options-clear() { - declare desc="clear a docker options from application" - declare cmd="docker-options:clear" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" PHASES="$2" - - verify_app_name "$APP" - - if [[ -z "$PHASES" ]]; then - dokku_log_info1 "Clearing docker-options for $APP on all phases" - rm -f "$(fn-get-phase-file-path "$APP" "build")" "$(fn-get-phase-file-path "$APP" "deploy")" "$(fn-get-phase-file-path "$APP" "run")" - return - fi - - read -ra passed_phases <<<"$(get_phases "$PHASES")" - for phase in "${passed_phases[@]}"; do - dokku_log_info1 "Clearing docker-options for $APP on phase $phase" - rm -f "$(fn-get-phase-file-path "$APP" "$phase")" - done -} - -cmd-docker-options-clear "$@" diff --git a/plugins/docker-options/subcommands/default b/plugins/docker-options/subcommands/default deleted file mode 100755 index 409079c9b..000000000 --- a/plugins/docker-options/subcommands/default +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_AVAILABLE_PATH/docker-options/help-functions" - -cmd-docker-options-help "docker-options:help" diff --git a/plugins/docker-options/subcommands/remove b/plugins/docker-options/subcommands/remove deleted file mode 100755 index bddbbcdbd..000000000 --- a/plugins/docker-options/subcommands/remove +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/docker-options/functions" - -cmd-docker-options-remove() { - declare desc="Remove a docker option from application" - declare cmd="docker-options:remove" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - - verify_app_name "$APP" - read -ra passed_phases <<<"$(get_phases "$2")" - shift 2 # everything else passed is the docker option - [[ -z ${passed_docker_option="$@"} ]] && dokku_log_fail "Please specify docker options to remove from the phase" - remove_passed_docker_option passed_phases[@] "${passed_docker_option[@]}" -} - -cmd-docker-options-remove "$@" diff --git a/plugins/docker-options/subcommands/report b/plugins/docker-options/subcommands/report deleted file mode 100755 index 5985f4add..000000000 --- a/plugins/docker-options/subcommands/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/docker-options/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-docker-options-report "$@" diff --git a/plugins/docker-options/triggers.go b/plugins/docker-options/triggers.go new file mode 100644 index 000000000..9e63a06fc --- /dev/null +++ b/plugins/docker-options/triggers.go @@ -0,0 +1,65 @@ +package dockeroptions + +import ( + "fmt" + "io" + "os" + + "github.com/dokku/dokku/plugins/common" +) + +// TriggerPostAppCloneSetup copies docker option files from the source app to the cloned app +func TriggerPostAppCloneSetup(oldAppName string, newAppName string) error { + for _, phase := range availablePhases { + if err := copyPhaseFile(oldAppName, newAppName, phase); err != nil { + return err + } + } + return nil +} + +// TriggerPostAppRenameSetup copies docker option files from the old app name to the new app name +func TriggerPostAppRenameSetup(oldAppName string, newAppName string) error { + for _, phase := range availablePhases { + if err := copyPhaseFile(oldAppName, newAppName, phase); err != nil { + return err + } + } + return nil +} + +// TriggerPostDelete deletes the docker option files for an app +func TriggerPostDelete(appName string) error { + for _, phase := range availablePhases { + if err := removePhaseFile(appName, phase); err != nil { + return err + } + } + return nil +} + +func copyPhaseFile(srcApp string, dstApp string, phase string) error { + srcPath := getPhaseFilePath(srcApp, phase) + if !common.FileExists(srcPath) { + return nil + } + + src, err := os.Open(srcPath) + if err != nil { + return fmt.Errorf("Unable to open docker options phase file %s.%s: %s", srcApp, phase, err.Error()) + } + defer src.Close() + + dstPath := getPhaseFilePath(dstApp, phase) + dst, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("Unable to create docker options phase file %s.%s: %s", dstApp, phase, err.Error()) + } + defer dst.Close() + + if _, err := io.Copy(dst, src); err != nil { + return fmt.Errorf("Unable to copy docker options phase file %s.%s to %s.%s: %s", srcApp, phase, dstApp, phase, err.Error()) + } + + return nil +} From b397cf1bd7c0af238cf19cc06c28d2ae347a23d1 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 27 Apr 2026 15:58:35 -0400 Subject: [PATCH 2/3] test: pass deploy phase to docker-options:add in udp test The single-udp network test was calling `docker-options:add $TEST_APP -p 1194:1194` without a phase argument. The bash subcommand silently no-op'd because `-p` is not a recognized phase, so the test happened to pass even though it was not actually setting the option. The go port surfaces the validation error correctly, so the test now needs the proper `deploy` phase to keep working. --- tests/unit/network.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/network.bats b/tests/unit/network.bats index db2831c65..166ddd084 100644 --- a/tests/unit/network.bats +++ b/tests/unit/network.bats @@ -327,7 +327,7 @@ teardown() { } @test "(network) handle single-udp app" { - run /bin/bash -c "dokku docker-options:add $TEST_APP -p 1194:1194" + run /bin/bash -c "dokku docker-options:add $TEST_APP deploy -p 1194:1194" echo "output: $output" echo "status: $status" assert_success From 87dd39e0b38871c5f2f195743643ad36ab7e2c20 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 27 Apr 2026 15:58:40 -0400 Subject: [PATCH 3/3] refactor: collect docker-options file helpers in functions.go Moves `copyPhaseFile` and `removePhaseFile` next to the other phase-file helpers so the package keeps a single home for filesystem plumbing. --- plugins/docker-options/functions.go | 34 +++++++++++++++++++++++++++ plugins/docker-options/subcommands.go | 8 ------- plugins/docker-options/triggers.go | 34 --------------------------- 3 files changed, 34 insertions(+), 42 deletions(-) diff --git a/plugins/docker-options/functions.go b/plugins/docker-options/functions.go index f00fa5546..57b2d38d3 100644 --- a/plugins/docker-options/functions.go +++ b/plugins/docker-options/functions.go @@ -2,6 +2,7 @@ package dockeroptions import ( "fmt" + "io" "os" "path/filepath" "strings" @@ -13,6 +14,39 @@ func getPhaseFilePath(appName string, phase string) string { return filepath.Join(common.AppRoot(appName), "DOCKER_OPTIONS_"+strings.ToUpper(phase)) } +func copyPhaseFile(srcApp string, dstApp string, phase string) error { + srcPath := getPhaseFilePath(srcApp, phase) + if !common.FileExists(srcPath) { + return nil + } + + src, err := os.Open(srcPath) + if err != nil { + return fmt.Errorf("Unable to open docker options phase file %s.%s: %s", srcApp, phase, err.Error()) + } + defer src.Close() + + dstPath := getPhaseFilePath(dstApp, phase) + dst, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("Unable to create docker options phase file %s.%s: %s", dstApp, phase, err.Error()) + } + defer dst.Close() + + if _, err := io.Copy(dst, src); err != nil { + return fmt.Errorf("Unable to copy docker options phase file %s.%s to %s.%s: %s", srcApp, phase, dstApp, phase, err.Error()) + } + + return nil +} + +func removePhaseFile(appName string, phase string) error { + if err := os.Remove(getPhaseFilePath(appName, phase)); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + func touchPhaseFile(appName string, phase string) error { phaseFilePath := getPhaseFilePath(appName, phase) diff --git a/plugins/docker-options/subcommands.go b/plugins/docker-options/subcommands.go index f68606245..2f72d7077 100644 --- a/plugins/docker-options/subcommands.go +++ b/plugins/docker-options/subcommands.go @@ -3,7 +3,6 @@ package dockeroptions import ( "errors" "fmt" - "os" "github.com/dokku/dokku/plugins/common" ) @@ -96,10 +95,3 @@ func CommandReport(appName string, format string, infoFlag string) error { return ReportSingleApp(appName, format, infoFlag) } - -func removePhaseFile(appName string, phase string) error { - if err := os.Remove(getPhaseFilePath(appName, phase)); err != nil && !os.IsNotExist(err) { - return err - } - return nil -} diff --git a/plugins/docker-options/triggers.go b/plugins/docker-options/triggers.go index 9e63a06fc..e594291f7 100644 --- a/plugins/docker-options/triggers.go +++ b/plugins/docker-options/triggers.go @@ -1,13 +1,5 @@ package dockeroptions -import ( - "fmt" - "io" - "os" - - "github.com/dokku/dokku/plugins/common" -) - // TriggerPostAppCloneSetup copies docker option files from the source app to the cloned app func TriggerPostAppCloneSetup(oldAppName string, newAppName string) error { for _, phase := range availablePhases { @@ -37,29 +29,3 @@ func TriggerPostDelete(appName string) error { } return nil } - -func copyPhaseFile(srcApp string, dstApp string, phase string) error { - srcPath := getPhaseFilePath(srcApp, phase) - if !common.FileExists(srcPath) { - return nil - } - - src, err := os.Open(srcPath) - if err != nil { - return fmt.Errorf("Unable to open docker options phase file %s.%s: %s", srcApp, phase, err.Error()) - } - defer src.Close() - - dstPath := getPhaseFilePath(dstApp, phase) - dst, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return fmt.Errorf("Unable to create docker options phase file %s.%s: %s", dstApp, phase, err.Error()) - } - defer dst.Close() - - if _, err := io.Copy(dst, src); err != nil { - return fmt.Errorf("Unable to copy docker options phase file %s.%s to %s.%s: %s", srcApp, phase, dstApp, phase, err.Error()) - } - - return nil -}