Merge pull request #8516 from dokku/2441-process-scoped-docker-options
Scope docker-options to specific procfile processes
This commit is contained in:
@@ -4,10 +4,11 @@
|
||||
> New as of 0.3.17
|
||||
|
||||
```
|
||||
docker-options:add <app> <phase(s)> OPTION # Add Docker option to app for phase (comma-separated phase list)
|
||||
docker-options:clear <app> [<phase(s)>...] # Clear a docker options from app
|
||||
docker-options:remove <app> <phase(s)> OPTION # Remove Docker option from app for phase (comma-separated phase list)
|
||||
docker-options:report [<app>] [<flag>] # Displays a docker options report for one or more apps
|
||||
docker-options:add [--process PROC...] <app> <phase(s)> OPTION # Add Docker option to app for phase
|
||||
docker-options:clear [--process PROC...] <app> [<phase(s)>...] # Clear docker options from app
|
||||
docker-options:list <app> [--process PROC] --phase PHASE # List docker options for one process+phase pair
|
||||
docker-options:remove [--process PROC...] <app> <phase(s)> OPTION # Remove Docker option from app for phase
|
||||
docker-options:report [<app>] [<flag>] [--format json|stdout] # Displays a docker options report for one or more apps
|
||||
```
|
||||
|
||||
The `docker-options` plugin allows users to specify custom [container options](https://docs.docker.com/engine/reference/run/) for containers created by Dokku at various phases.
|
||||
@@ -160,3 +161,67 @@ You can pass flags which will output only the value of the specific information
|
||||
```shell
|
||||
dokku docker-options:report node-js-app --docker-options-build
|
||||
```
|
||||
|
||||
When process-specific options are configured (see below), the report exposes one additional dynamic flag per configured `process.deploy` pair, named `--docker-options-deploy.<process>`:
|
||||
|
||||
```shell
|
||||
dokku docker-options:report node-js-app --docker-options-deploy.web
|
||||
```
|
||||
|
||||
A machine-readable JSON view is available via `--format json`:
|
||||
|
||||
```shell
|
||||
dokku docker-options:report node-js-app --format json
|
||||
```
|
||||
|
||||
### Process-Specific Options
|
||||
|
||||
> [!IMPORTANT]
|
||||
> New as of 0.38.0
|
||||
|
||||
Docker options can be scoped to specific process types declared in the app's `Procfile` by passing one or more `--process` flags. This is useful when a deploy-phase option (for example a port mapping) makes sense for one process type but would conflict with another - the canonical case being a `web` process that needs `-p 6789:5000` published while the `worker` process must not bind that port.
|
||||
|
||||
Process scoping is supported only for the `deploy` phase. The `build` phase runs once per app and the `run` phase covers ad-hoc commands and cron tasks where no Procfile process type is in play; both reject `--process`.
|
||||
|
||||
There is no `--global` flag. Omitting `--process` keeps the historical behavior of applying the option to every container in the app. Avoiding a `--global` flag here is intentional: elsewhere in Dokku `--global` means "across all apps" (e.g. `dokku config:set --global`), which would be misleading in this plugin where the scope is always one app.
|
||||
|
||||
#### Setting process-specific options
|
||||
|
||||
```shell
|
||||
# Add a port mapping only to the web process
|
||||
dokku docker-options:add --process web node-js-app deploy "-p 6789:5000"
|
||||
|
||||
# Add a GPU mount only to the worker process
|
||||
dokku docker-options:add --process worker node-js-app deploy "--gpus all"
|
||||
```
|
||||
|
||||
Multiple `--process` flags can be combined to apply the same option to several process types in one call:
|
||||
|
||||
```shell
|
||||
dokku docker-options:add --process web --process api node-js-app deploy "-v /shared:/shared"
|
||||
```
|
||||
|
||||
If `--process` names a process type that is not currently declared in the app's `Procfile`, the command succeeds but emits a warning. This allows configuring options ahead of a deploy that adds the new process type.
|
||||
|
||||
The `_default_` value is reserved internally and cannot be passed to `--process`.
|
||||
|
||||
#### Removing and clearing process-specific options
|
||||
|
||||
```shell
|
||||
# Remove a single option from one process
|
||||
dokku docker-options:remove --process web node-js-app deploy "-p 6789:5000"
|
||||
|
||||
# Clear every option for a process+phase
|
||||
dokku docker-options:clear --process worker node-js-app deploy
|
||||
```
|
||||
|
||||
Without `--process`, `:remove` and `:clear` operate on the default scope only - per-process lists are left untouched.
|
||||
|
||||
#### Listing options for a process and phase
|
||||
|
||||
The `docker-options:list` command prints the options stored for a single process+phase pair, one option per line. Omitting `--process` lists the default scope.
|
||||
|
||||
```shell
|
||||
dokku docker-options:list node-js-app --process web --phase deploy
|
||||
dokku docker-options:list node-js-app --phase deploy
|
||||
```
|
||||
|
||||
@@ -809,7 +809,7 @@ APP="$1"; IMAGE_SOURCE_TYPE="$2"
|
||||
|
||||
### `docker-args-process-deploy`
|
||||
|
||||
- Description: `$PROC_TYPE` may be set to magic `_all_` process type to signify global docker deploy options.
|
||||
- Description: emits docker arguments scoped to a specific Procfile process type. The `docker-options` plugin implements this trigger to surface options registered via `docker-options:add --process <PROC>`. `$PROC_TYPE` may be empty (or set to the magic `_default_` value) to signify default-scope docker deploy options.
|
||||
- Invoked by: `dokku deploy`
|
||||
- Arguments: `$APP $IMAGE_SOURCE_TYPE $IMAGE_TAG [$PROC_TYPE $CONTAINER_INDEX]`
|
||||
- Example:
|
||||
|
||||
11
plugins/docker-options/.gitignore
vendored
Normal file
11
plugins/docker-options/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/commands
|
||||
/subcommands/*
|
||||
/triggers/*
|
||||
/triggers
|
||||
/install
|
||||
/post-*
|
||||
/report
|
||||
/docker-args-build
|
||||
/docker-args-deploy
|
||||
/docker-args-process-deploy
|
||||
/docker-args-run
|
||||
@@ -1,5 +1,5 @@
|
||||
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
|
||||
SUBCOMMANDS = subcommands/add subcommands/clear subcommands/list subcommands/remove subcommands/report
|
||||
TRIGGERS = triggers/docker-args-build triggers/docker-args-deploy triggers/docker-args-process-deploy triggers/docker-args-run triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-delete triggers/report
|
||||
BUILD = commands subcommands triggers
|
||||
PLUGIN_NAME = docker-options
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
docker-args-deploy
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
[[ $DOKKU_TRACE ]] && set -x
|
||||
|
||||
trigger-docker-options-docker-args() {
|
||||
declare desc="docker args plugin trigger"
|
||||
declare trigger="$0"
|
||||
declare APP="$1" IMAGE_SOURCE_TYPE="$2"
|
||||
local STDIN=$(cat)
|
||||
|
||||
case "$0" in
|
||||
*docker-args-build)
|
||||
local PHASE=BUILD
|
||||
;;
|
||||
*docker-args-deploy)
|
||||
local PHASE=DEPLOY
|
||||
;;
|
||||
*docker-args-run)
|
||||
local PHASE=RUN
|
||||
;;
|
||||
esac
|
||||
|
||||
local FILE_PREFIX="DOCKER_OPTIONS_"
|
||||
local PHASE_FILE_PATH="${DOKKU_ROOT}/${APP}/${FILE_PREFIX}${PHASE}"
|
||||
|
||||
local output=""
|
||||
|
||||
if [[ -f "$PHASE_FILE_PATH" ]]; then
|
||||
local DONE=false
|
||||
until $DONE; do
|
||||
local line
|
||||
read -r line || local DONE=true
|
||||
|
||||
[[ -z "$line" ]] && continue
|
||||
|
||||
case "$line" in
|
||||
\#*)
|
||||
continue
|
||||
;;
|
||||
|
||||
--restart*)
|
||||
if [[ "$PHASE" == "DEPLOY" ]]; then
|
||||
local output="$output $line"
|
||||
fi
|
||||
continue
|
||||
;;
|
||||
|
||||
*)
|
||||
case "$IMAGE_SOURCE_TYPE" in
|
||||
dockerfile | nixpacks | railpack)
|
||||
case "$line" in
|
||||
--link* | -v* | --volume*)
|
||||
continue
|
||||
;;
|
||||
|
||||
*)
|
||||
local output="$output $line"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
herokuish)
|
||||
case "$line" in
|
||||
--file* | --build-args*)
|
||||
continue
|
||||
;;
|
||||
|
||||
*)
|
||||
local output="$output $line"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
*)
|
||||
local output="$output $line"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done <"$PHASE_FILE_PATH"
|
||||
fi
|
||||
|
||||
echo -n "$STDIN$output"
|
||||
}
|
||||
|
||||
trigger-docker-options-docker-args "$@"
|
||||
@@ -1 +0,0 @@
|
||||
docker-args-deploy
|
||||
@@ -1,119 +1,152 @@
|
||||
package dockeroptions
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
// SetDockerOptionForPhases sets an option to specified phases
|
||||
// DefaultProcessType is the sentinel process-type key used for options that
|
||||
// apply to every container in an app (i.e. options not scoped to a specific
|
||||
// Procfile process type).
|
||||
const DefaultProcessType = "_default_"
|
||||
|
||||
func propertyKey(processType, phase string) string {
|
||||
if processType == "" {
|
||||
processType = DefaultProcessType
|
||||
}
|
||||
return fmt.Sprintf("%s.%s", processType, phase)
|
||||
}
|
||||
|
||||
// SetDockerOptionForPhases sets a `--name=value` option in the default scope
|
||||
// for the specified phases, replacing any existing entry with the same name.
|
||||
func SetDockerOptionForPhases(appName string, phases []string, name string, value string) error {
|
||||
for _, phase := range phases {
|
||||
if err := touchPhaseFile(appName, phase); err != nil {
|
||||
return err
|
||||
}
|
||||
return SetDockerOptionForProcessPhases(appName, []string{DefaultProcessType}, phases, name, value)
|
||||
}
|
||||
|
||||
options, err := GetDockerOptionsForPhase(appName, phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newOptions := []string{}
|
||||
for _, option := range options {
|
||||
if strings.HasPrefix(option, fmt.Sprintf("--%s=", name)) {
|
||||
continue
|
||||
// SetDockerOptionForProcessPhases sets a `--name=value` option for the specified
|
||||
// process types and phases, replacing any existing entry with the same name.
|
||||
func SetDockerOptionForProcessPhases(appName string, processTypes []string, phases []string, name string, value string) error {
|
||||
if len(processTypes) == 0 {
|
||||
processTypes = []string{DefaultProcessType}
|
||||
}
|
||||
for _, processType := range processTypes {
|
||||
for _, phase := range phases {
|
||||
options, err := GetDockerOptionsForProcessPhase(appName, processType, phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newOptions = append(newOptions, option)
|
||||
}
|
||||
newOptions := []string{}
|
||||
for _, option := range options {
|
||||
if strings.HasPrefix(option, fmt.Sprintf("--%s=", name)) {
|
||||
continue
|
||||
}
|
||||
newOptions = append(newOptions, option)
|
||||
}
|
||||
|
||||
newOptions = append(newOptions, fmt.Sprintf("--%s=%s", name, value))
|
||||
sort.Strings(newOptions)
|
||||
if err = writeDockerOptionsForPhase(appName, phase, newOptions); err != nil {
|
||||
return err
|
||||
newOptions = append(newOptions, fmt.Sprintf("--%s=%s", name, value))
|
||||
sort.Strings(newOptions)
|
||||
if err := writeDockerOptionsForProcessPhase(appName, processType, phase, newOptions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddDockerOptionToPhases adds docker option to specified phases
|
||||
// AddDockerOptionToPhases adds an option to the default scope for the specified phases.
|
||||
func AddDockerOptionToPhases(appName string, phases []string, option string) error {
|
||||
for _, phase := range phases {
|
||||
if err := touchPhaseFile(appName, phase); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options, err := GetDockerOptionsForPhase(appName, phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options = append(options, option)
|
||||
sort.Strings(options)
|
||||
if err = writeDockerOptionsForPhase(appName, phase, options); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return AddDockerOptionToProcessPhases(appName, []string{DefaultProcessType}, phases, option)
|
||||
}
|
||||
|
||||
// GetDockerOptionsForPhase returns the docker options for the specified phase
|
||||
func GetDockerOptionsForPhase(appName string, phase string) ([]string, error) {
|
||||
options := []string{}
|
||||
|
||||
if err := touchPhaseFile(appName, phase); err != nil {
|
||||
return options, err
|
||||
// AddDockerOptionToProcessPhases adds an option to the specified process types and phases.
|
||||
func AddDockerOptionToProcessPhases(appName string, processTypes []string, phases []string, option string) error {
|
||||
if len(processTypes) == 0 {
|
||||
processTypes = []string{DefaultProcessType}
|
||||
}
|
||||
for _, processType := range processTypes {
|
||||
for _, phase := range phases {
|
||||
options, err := GetDockerOptionsForProcessPhase(appName, processType, phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
phaseFilePath := getPhaseFilePath(appName, phase)
|
||||
file, err := os.Open(phaseFilePath)
|
||||
if err != nil {
|
||||
return options, fmt.Errorf("Unable to open docker options phase file %s.%s: %s", appName, appName, err.Error())
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
options = append(options, strings.TrimSpace(scanner.Text()))
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return options, fmt.Errorf("Unable to read docker options phase file %s.%s: %s", appName, appName, err.Error())
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
// RemoveDockerOptionFromPhases removes a docker option from specified phases
|
||||
func RemoveDockerOptionFromPhases(appName string, phases []string, option string) error {
|
||||
for _, phase := range phases {
|
||||
options, err := GetDockerOptionsForPhase(appName, phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newOptions := []string{}
|
||||
for _, opt := range options {
|
||||
if opt != option {
|
||||
newOptions = append(newOptions, opt)
|
||||
options = append(options, option)
|
||||
sort.Strings(options)
|
||||
if err := writeDockerOptionsForProcessPhase(appName, processType, phase, options); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
sort.Strings(newOptions)
|
||||
if err = writeDockerOptionsForPhase(appName, phase, newOptions); err != nil {
|
||||
return err
|
||||
// GetDockerOptionsForPhase returns the docker options stored under the default
|
||||
// scope for the specified phase.
|
||||
func GetDockerOptionsForPhase(appName string, phase string) ([]string, error) {
|
||||
return GetDockerOptionsForProcessPhase(appName, DefaultProcessType, phase)
|
||||
}
|
||||
|
||||
// GetDockerOptionsForProcessPhase returns the docker options stored under the
|
||||
// given process-type scope for the specified phase. An empty processType is
|
||||
// treated as the default scope.
|
||||
func GetDockerOptionsForProcessPhase(appName, processType, phase string) ([]string, error) {
|
||||
options, err := common.PropertyListGet("docker-options", appName, propertyKey(processType, phase))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to read docker options for %s.%s.%s: %s", appName, processType, phase, err.Error())
|
||||
}
|
||||
|
||||
trimmed := make([]string, 0, len(options))
|
||||
for _, option := range options {
|
||||
option = strings.TrimSpace(option)
|
||||
if option == "" {
|
||||
continue
|
||||
}
|
||||
trimmed = append(trimmed, option)
|
||||
}
|
||||
return trimmed, nil
|
||||
}
|
||||
|
||||
// RemoveDockerOptionFromPhases removes an option from the default scope for the specified phases.
|
||||
func RemoveDockerOptionFromPhases(appName string, phases []string, option string) error {
|
||||
return RemoveDockerOptionFromProcessPhases(appName, []string{DefaultProcessType}, phases, option)
|
||||
}
|
||||
|
||||
// RemoveDockerOptionFromProcessPhases removes an option from the specified process types and phases.
|
||||
func RemoveDockerOptionFromProcessPhases(appName string, processTypes []string, phases []string, option string) error {
|
||||
if len(processTypes) == 0 {
|
||||
processTypes = []string{DefaultProcessType}
|
||||
}
|
||||
for _, processType := range processTypes {
|
||||
for _, phase := range phases {
|
||||
options, err := GetDockerOptionsForProcessPhase(appName, processType, phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newOptions := []string{}
|
||||
for _, opt := range options {
|
||||
if opt != option {
|
||||
newOptions = append(newOptions, opt)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(newOptions)
|
||||
if err := writeDockerOptionsForProcessPhase(appName, processType, phase, newOptions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSpecifiedDockerOptionsForPhase returns the docker options for the specified phase that are in the desiredOptions list
|
||||
// It expects desiredOptions to be a list of docker options that are in the format "--option"
|
||||
// And will retrieve any lines that start with the desired option
|
||||
// GetSpecifiedDockerOptionsForPhase returns the docker options for the specified
|
||||
// phase (default scope) that are in the desiredOptions list. It expects
|
||||
// desiredOptions entries in the form "--option" and matches against options
|
||||
// stored as "--option", "--option=value", or "--option value".
|
||||
func GetSpecifiedDockerOptionsForPhase(appName string, phase string, desiredOptions []string) (map[string][]string, error) {
|
||||
foundOptions := map[string][]string{}
|
||||
options, err := GetDockerOptionsForPhase(appName, phase)
|
||||
@@ -128,7 +161,6 @@ func GetSpecifiedDockerOptionsForPhase(appName string, phase string, desiredOpti
|
||||
break
|
||||
}
|
||||
|
||||
// match options that are in the format "--option=value"
|
||||
if strings.HasPrefix(option, fmt.Sprintf("%s=", desiredOption)) {
|
||||
if _, ok := foundOptions[desiredOption]; !ok {
|
||||
foundOptions[desiredOption] = []string{}
|
||||
@@ -144,7 +176,6 @@ func GetSpecifiedDockerOptionsForPhase(appName string, phase string, desiredOpti
|
||||
break
|
||||
}
|
||||
|
||||
// match options that are in the format "--option value"
|
||||
if strings.HasPrefix(option, fmt.Sprintf("%s ", desiredOption)) {
|
||||
if _, ok := foundOptions[desiredOption]; !ok {
|
||||
foundOptions[desiredOption] = []string{}
|
||||
@@ -164,3 +195,48 @@ func GetSpecifiedDockerOptionsForPhase(appName string, phase string, desiredOpti
|
||||
|
||||
return foundOptions, nil
|
||||
}
|
||||
|
||||
// ListProcessTypesWithOptions returns the sorted list of process types that
|
||||
// have at least one option configured, excluding DefaultProcessType.
|
||||
func ListProcessTypesWithOptions(appName string) ([]string, error) {
|
||||
properties, err := common.PropertyGetAll("docker-options", appName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
for key := range properties {
|
||||
processType, _, ok := splitPropertyKey(key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if processType == DefaultProcessType {
|
||||
continue
|
||||
}
|
||||
seen[processType] = true
|
||||
}
|
||||
|
||||
processTypes := make([]string, 0, len(seen))
|
||||
for processType := range seen {
|
||||
processTypes = append(processTypes, processType)
|
||||
}
|
||||
sort.Strings(processTypes)
|
||||
return processTypes, nil
|
||||
}
|
||||
|
||||
func splitPropertyKey(key string) (processType, phase string, ok bool) {
|
||||
idx := strings.LastIndex(key, ".")
|
||||
if idx <= 0 || idx == len(key)-1 {
|
||||
return "", "", false
|
||||
}
|
||||
processType = key[:idx]
|
||||
phase = key[idx+1:]
|
||||
if !isValidPhase(phase) {
|
||||
return "", "", false
|
||||
}
|
||||
return processType, phase, true
|
||||
}
|
||||
|
||||
func writeDockerOptionsForProcessPhase(appName, processType, phase string, options []string) error {
|
||||
return common.PropertyListWrite("docker-options", appName, propertyKey(processType, phase), options)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package dockeroptions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -10,65 +10,258 @@ import (
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
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) {
|
||||
// CommandAdd adds a docker option to the specified phases for an app.
|
||||
// When processes is empty the option is added to the default scope (apply
|
||||
// to every container in the app); otherwise it is added to each named
|
||||
// process type. The default and process flows are mutually exclusive
|
||||
// because process scoping is only valid for the deploy phase.
|
||||
func CommandAdd(appName string, processes []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")
|
||||
}
|
||||
|
||||
if err := ValidateProcessFlag(processes, phases); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, processType := range processes {
|
||||
WarnIfProcessNotInProcfile(appName, processType)
|
||||
}
|
||||
|
||||
if len(processes) == 0 {
|
||||
return AddDockerOptionToPhases(appName, phases, option)
|
||||
}
|
||||
|
||||
return AddDockerOptionToProcessPhases(appName, processes, phases, option)
|
||||
}
|
||||
|
||||
// CommandRemove removes a docker option from the specified phases for an app.
|
||||
// Process-flag handling matches CommandAdd.
|
||||
func CommandRemove(appName string, processes []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")
|
||||
}
|
||||
|
||||
if err := ValidateProcessFlag(processes, phases); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(processes) == 0 {
|
||||
return RemoveDockerOptionFromPhases(appName, phases, option)
|
||||
}
|
||||
|
||||
return RemoveDockerOptionFromProcessPhases(appName, processes, phases, option)
|
||||
}
|
||||
|
||||
// CommandClear removes all docker options for an app, optionally limited to
|
||||
// a list of phases and/or specific process types. With no flags it clears
|
||||
// the default scope across all phases; with --process flags it clears each
|
||||
// named process type for the supplied (deploy-only) phases.
|
||||
func CommandClear(appName string, processes []string, phasesArg string) error {
|
||||
if err := common.VerifyAppName(appName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(processes) == 0 {
|
||||
return clearDefaultScope(appName, phasesArg)
|
||||
}
|
||||
|
||||
phases, err := parsePhases(phasesArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(phases) == 0 {
|
||||
phases = []string{"deploy"}
|
||||
}
|
||||
|
||||
if err := ValidateProcessFlag(processes, phases); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, processType := range processes {
|
||||
for _, phase := range phases {
|
||||
common.LogInfo1(fmt.Sprintf("Clearing docker-options for %s on phase %s for process %s", appName, phase, processType))
|
||||
if err := common.PropertyDelete("docker-options", appName, propertyKey(processType, phase)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func touchPhaseFile(appName string, phase string) error {
|
||||
phaseFilePath := getPhaseFilePath(appName, phase)
|
||||
|
||||
_, err := os.Stat(phaseFilePath)
|
||||
if !os.IsNotExist(err) {
|
||||
func clearDefaultScope(appName string, phasesArg string) error {
|
||||
if phasesArg == "" {
|
||||
common.LogInfo1(fmt.Sprintf("Clearing docker-options for %s on all phases", appName))
|
||||
for _, phase := range availablePhases {
|
||||
if err := common.PropertyDelete("docker-options", appName, propertyKey(DefaultProcessType, phase)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Create(phaseFilePath)
|
||||
phases, err := parsePhases(phasesArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to create docker options phase file %s.%s: %s", appName, phase, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
for _, phase := range phases {
|
||||
common.LogInfo1(fmt.Sprintf("Clearing docker-options for %s on phase %s", appName, phase))
|
||||
if err := common.PropertyDelete("docker-options", appName, propertyKey(DefaultProcessType, phase)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeDockerOptionsForPhase(appName string, phase string, options []string) error {
|
||||
phaseFilePath := getPhaseFilePath(appName, phase)
|
||||
return common.WriteSliceToFile(common.WriteSliceToFileInput{
|
||||
Filename: phaseFilePath,
|
||||
Lines: options,
|
||||
Mode: os.FileMode(0600),
|
||||
})
|
||||
// CommandList prints the docker options stored for a given process+phase, one
|
||||
// option per line. An empty processType means the default scope.
|
||||
func CommandList(appName, processType, phase string) error {
|
||||
if err := common.VerifyAppName(appName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if phase == "" {
|
||||
return errors.New("--phase is required")
|
||||
}
|
||||
|
||||
if !isValidPhase(phase) {
|
||||
return fmt.Errorf("Phase must be one of [%s]", strings.Join(availablePhases, " "))
|
||||
}
|
||||
|
||||
if processType != "" {
|
||||
if processType == DefaultProcessType {
|
||||
return fmt.Errorf("%q is reserved and cannot be used as a --process value", DefaultProcessType)
|
||||
}
|
||||
if !processScopedPhases[phase] {
|
||||
return fmt.Errorf("--process is only supported for the deploy phase, got %q", phase)
|
||||
}
|
||||
}
|
||||
|
||||
options, err := GetDockerOptionsForProcessPhase(appName, processType, phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
fmt.Println(option)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// 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") {
|
||||
return nil
|
||||
}
|
||||
|
||||
apps, err := common.DokkuApps()
|
||||
if err != nil {
|
||||
if errors.Is(err, common.NoAppsExist) {
|
||||
return common.PropertyWrite("docker-options", "--global", "migrated-from-files", "true")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, appName := range apps {
|
||||
for _, phase := range availablePhases {
|
||||
legacyPath := filepath.Join(common.AppRoot(appName), "DOCKER_OPTIONS_"+strings.ToUpper(phase))
|
||||
if !common.FileExists(legacyPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
lines, err := readLegacyOptionsFile(legacyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := common.PropertyListWrite("docker-options", appName, propertyKey(DefaultProcessType, phase), lines); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migratedPath := legacyPath + ".migrated"
|
||||
if err := os.Rename(legacyPath, migratedPath); err != nil {
|
||||
return fmt.Errorf("Unable to rename migrated file %s: %s", legacyPath, err.Error())
|
||||
}
|
||||
|
||||
common.LogInfo1(fmt.Sprintf("Migrated %s to docker-options properties", legacyPath))
|
||||
}
|
||||
}
|
||||
|
||||
return common.PropertyWrite("docker-options", "--global", "migrated-from-files", "true")
|
||||
}
|
||||
|
||||
func readLegacyOptionsFile(path string) ([]string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to read %s: %s", path, err.Error())
|
||||
}
|
||||
|
||||
var lines []string
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, trimmed)
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func removeMigratedLegacyFiles(appName string) {
|
||||
for _, phase := range availablePhases {
|
||||
path := filepath.Join(common.AppRoot(appName), "DOCKER_OPTIONS_"+strings.ToUpper(phase)+".migrated")
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,31 @@ package dockeroptions
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
var availablePhases = []string{"build", "deploy", "run"}
|
||||
|
||||
// processScopedPhases lists the phases that may be scoped via --process.
|
||||
// Only deploy is scoped: build runs once per app (no process-type concept) and
|
||||
// run is invoked for ad-hoc commands and cron tasks where the caller does not
|
||||
// supply a Procfile process type.
|
||||
var processScopedPhases = map[string]bool{
|
||||
"deploy": true,
|
||||
}
|
||||
|
||||
func isValidPhase(phase string) bool {
|
||||
for _, p := range availablePhases {
|
||||
if p == phase {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parsePhases(phasesArg string) ([]string, error) {
|
||||
if phasesArg == "" {
|
||||
return nil, nil
|
||||
@@ -15,14 +35,7 @@ func parsePhases(phasesArg string) ([]string, error) {
|
||||
|
||||
phases := strings.Split(phasesArg, ",")
|
||||
for _, phase := range phases {
|
||||
valid := false
|
||||
for _, allowed := range availablePhases {
|
||||
if phase == allowed {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
if !isValidPhase(phase) {
|
||||
return nil, fmt.Errorf("Phase(s) must be one of [%s]", strings.Join(availablePhases, " "))
|
||||
}
|
||||
}
|
||||
@@ -30,16 +43,55 @@ func parsePhases(phasesArg string) ([]string, error) {
|
||||
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 {
|
||||
// ValidateProcessFlag rejects invalid combinations of --process and the
|
||||
// supplied phases. When processes is empty the call applies to the default
|
||||
// scope and any phase is allowed.
|
||||
func ValidateProcessFlag(processes []string, phases []string) error {
|
||||
if len(processes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(processes) > 0 && global {
|
||||
return errors.New("--process and --global flags are mutually exclusive")
|
||||
for _, processType := range processes {
|
||||
if processType == "" {
|
||||
return errors.New("--process value must not be empty")
|
||||
}
|
||||
if processType == DefaultProcessType {
|
||||
return fmt.Errorf("%q is reserved and cannot be used as a --process value", DefaultProcessType)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("--process and --global flags are reserved for a future release and not yet implemented")
|
||||
for _, phase := range phases {
|
||||
if !processScopedPhases[phase] {
|
||||
return fmt.Errorf("--process is only supported for the deploy phase, got %q", phase)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WarnIfProcessNotInProcfile emits a warning (without failing) when the named
|
||||
// process type is not present in the app's current Procfile. The check is
|
||||
// best-effort: if the Procfile or procfile-util are unavailable, the function
|
||||
// returns silently.
|
||||
func WarnIfProcessNotInProcfile(appName, processType string) {
|
||||
procfilePath := filepath.Join(common.GetAppDataDirectory("ps", appName), "Procfile")
|
||||
if !common.FileExists(procfilePath) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := common.CallExecCommand(common.ExecCommandInput{
|
||||
Command: "procfile-util",
|
||||
Args: []string{"list", "--procfile", procfilePath},
|
||||
})
|
||||
if err != nil || result.ExitCode != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(strings.TrimSpace(result.StdoutContents()), "\n") {
|
||||
if strings.TrimSpace(line) == processType {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
common.LogWarn(fmt.Sprintf("Process type %q is not declared in the Procfile for %s", processType, appName))
|
||||
}
|
||||
|
||||
148
plugins/docker-options/migration_test.go
Normal file
148
plugins/docker-options/migration_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package dockeroptions
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
// setupMigrationEnv 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.
|
||||
func setupMigrationEnv(t *testing.T) (dokkuRoot 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
|
||||
}
|
||||
|
||||
func writeLegacyDockerOptionsFile(t *testing.T, dokkuRoot, app, phase, contents string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Join(dokkuRoot, app), 0755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
path := filepath.Join(dokkuRoot, app, "DOCKER_OPTIONS_"+phase)
|
||||
if err := os.WriteFile(path, []byte(contents), 0644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateLegacyDockerOptionsFiles_MigratesAndIsIdempotent(t *testing.T) {
|
||||
dokkuRoot := setupMigrationEnv(t)
|
||||
|
||||
writeLegacyDockerOptionsFile(t, dokkuRoot, "alpha", "DEPLOY", "-v /var/log:/log\n# a comment\n\n--restart=on-failure:5\n")
|
||||
writeLegacyDockerOptionsFile(t, dokkuRoot, "alpha", "BUILD", "--build-arg FOO=bar\n")
|
||||
writeLegacyDockerOptionsFile(t, dokkuRoot, "beta", "DEPLOY", "-p 8080:5000\n")
|
||||
|
||||
if err := migrateLegacyDockerOptionsFiles(); err != nil {
|
||||
t.Fatalf("first migration: %v", err)
|
||||
}
|
||||
|
||||
deploy, err := common.PropertyListGet("docker-options", "alpha", "_default_.deploy")
|
||||
if err != nil {
|
||||
t.Fatalf("PropertyListGet alpha deploy: %v", err)
|
||||
}
|
||||
wantDeploy := []string{"-v /var/log:/log", "--restart=on-failure:5"}
|
||||
if !equalStrings(deploy, wantDeploy) {
|
||||
t.Errorf("alpha deploy = %v, want %v", deploy, wantDeploy)
|
||||
}
|
||||
|
||||
build, err := common.PropertyListGet("docker-options", "alpha", "_default_.build")
|
||||
if err != nil {
|
||||
t.Fatalf("PropertyListGet alpha build: %v", err)
|
||||
}
|
||||
wantBuild := []string{"--build-arg FOO=bar"}
|
||||
if !equalStrings(build, wantBuild) {
|
||||
t.Errorf("alpha build = %v, want %v", build, wantBuild)
|
||||
}
|
||||
|
||||
betaDeploy, err := common.PropertyListGet("docker-options", "beta", "_default_.deploy")
|
||||
if err != nil {
|
||||
t.Fatalf("PropertyListGet beta deploy: %v", err)
|
||||
}
|
||||
wantBetaDeploy := []string{"-p 8080:5000"}
|
||||
if !equalStrings(betaDeploy, wantBetaDeploy) {
|
||||
t.Errorf("beta deploy = %v, want %v", betaDeploy, wantBetaDeploy)
|
||||
}
|
||||
|
||||
if !common.PropertyExists("docker-options", "--global", "migrated-from-files") {
|
||||
t.Errorf("migrated-from-files marker not set after first migration")
|
||||
}
|
||||
|
||||
for _, app := range []string{"alpha", "beta"} {
|
||||
for _, phase := range []string{"BUILD", "DEPLOY", "RUN"} {
|
||||
legacy := filepath.Join(dokkuRoot, app, "DOCKER_OPTIONS_"+phase)
|
||||
migrated := legacy + ".migrated"
|
||||
if app == "alpha" && (phase == "BUILD" || phase == "DEPLOY") || app == "beta" && phase == "DEPLOY" {
|
||||
if _, err := os.Stat(migrated); err != nil {
|
||||
t.Errorf("expected %s to exist: %v", migrated, err)
|
||||
}
|
||||
if _, err := os.Stat(legacy); !os.IsNotExist(err) {
|
||||
t.Errorf("expected %s to be gone, got err=%v", legacy, err)
|
||||
}
|
||||
} else {
|
||||
if _, err := os.Stat(legacy); !os.IsNotExist(err) {
|
||||
t.Errorf("did not expect %s to exist, got err=%v", legacy, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sneakyPath := filepath.Join(dokkuRoot, "alpha", "DOCKER_OPTIONS_DEPLOY")
|
||||
sneakyContents := "-v /tmp/sneaky:/sneaky\n"
|
||||
if err := os.WriteFile(sneakyPath, []byte(sneakyContents), 0644); err != nil {
|
||||
t.Fatalf("re-create legacy file: %v", err)
|
||||
}
|
||||
|
||||
if err := migrateLegacyDockerOptionsFiles(); err != nil {
|
||||
t.Fatalf("second migration: %v", err)
|
||||
}
|
||||
|
||||
deployAfter, err := common.PropertyListGet("docker-options", "alpha", "_default_.deploy")
|
||||
if err != nil {
|
||||
t.Fatalf("PropertyListGet alpha deploy (post re-run): %v", err)
|
||||
}
|
||||
if !equalStrings(deployAfter, wantDeploy) {
|
||||
t.Errorf("alpha deploy after rerun = %v, want %v (idempotency violated)", deployAfter, wantDeploy)
|
||||
}
|
||||
|
||||
gotSneaky, err := os.ReadFile(sneakyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("expected sneaky legacy file untouched, got err=%v", err)
|
||||
}
|
||||
if string(gotSneaky) != sneakyContents {
|
||||
t.Errorf("sneaky legacy file modified: %q", gotSneaky)
|
||||
}
|
||||
}
|
||||
|
||||
func equalStrings(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
package dockeroptions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
// ReportSingleApp displays the docker options report for a single app
|
||||
// ReportSingleApp displays the docker options report for a single app.
|
||||
// Default-scope options are reported under fixed keys for each phase.
|
||||
// Process-scoped options surface as dynamic per-process keys, one per
|
||||
// configured process+phase combination.
|
||||
func ReportSingleApp(appName string, format string, infoFlag string) error {
|
||||
if err := common.VerifyAppName(appName); err != nil {
|
||||
return err
|
||||
@@ -18,6 +22,18 @@ func ReportSingleApp(appName string, format string, infoFlag string) error {
|
||||
"--docker-options-run": reportRunOptions,
|
||||
}
|
||||
|
||||
processTypes, err := ListProcessTypesWithOptions(appName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, processType := range processTypes {
|
||||
processType := processType
|
||||
flagName := fmt.Sprintf("--docker-options-deploy.%s", processType)
|
||||
flags[flagName] = func(app string) string {
|
||||
return joinProcessPhaseOptions(app, processType, "deploy")
|
||||
}
|
||||
}
|
||||
|
||||
flagKeys := []string{}
|
||||
for flagKey := range flags {
|
||||
flagKeys = append(flagKeys, flagKey)
|
||||
@@ -30,19 +46,19 @@ func ReportSingleApp(appName string, format string, infoFlag string) error {
|
||||
}
|
||||
|
||||
func reportBuildOptions(appName string) string {
|
||||
return joinPhaseOptions(appName, "build")
|
||||
return joinProcessPhaseOptions(appName, DefaultProcessType, "build")
|
||||
}
|
||||
|
||||
func reportDeployOptions(appName string) string {
|
||||
return joinPhaseOptions(appName, "deploy")
|
||||
return joinProcessPhaseOptions(appName, DefaultProcessType, "deploy")
|
||||
}
|
||||
|
||||
func reportRunOptions(appName string) string {
|
||||
return joinPhaseOptions(appName, "run")
|
||||
return joinProcessPhaseOptions(appName, DefaultProcessType, "run")
|
||||
}
|
||||
|
||||
func joinPhaseOptions(appName string, phase string) string {
|
||||
options, err := GetDockerOptionsForPhase(appName, phase)
|
||||
func joinProcessPhaseOptions(appName, processType, phase string) string {
|
||||
options, err := GetDockerOptionsForProcessPhase(appName, processType, phase)
|
||||
if err != nil || len(options) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -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,15 @@ 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)
|
||||
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 ]")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
package dockeroptions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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)
|
||||
}
|
||||
@@ -1,31 +1,141 @@
|
||||
package dockeroptions
|
||||
|
||||
// TriggerPostAppCloneSetup copies docker option files from the source app to the cloned app
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
// TriggerInstall sets up the docker-options property directory and migrates
|
||||
// any pre-existing DOCKER_OPTIONS_* files into property lists.
|
||||
func TriggerInstall() error {
|
||||
if err := common.PropertySetup("docker-options"); err != nil {
|
||||
return fmt.Errorf("Unable to install the docker-options plugin: %v", err)
|
||||
}
|
||||
|
||||
if err := migrateLegacyDockerOptionsFiles(); err != nil {
|
||||
return fmt.Errorf("Unable to migrate legacy docker-options files: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerPostAppCloneSetup copies docker option properties 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
|
||||
return common.PropertyClone("docker-options", oldAppName, newAppName)
|
||||
}
|
||||
|
||||
// TriggerPostAppRenameSetup copies docker option files from the old app name to the new app name
|
||||
// TriggerPostAppRenameSetup moves docker option properties from the old app
|
||||
// name to the new one.
|
||||
func TriggerPostAppRenameSetup(oldAppName string, newAppName string) error {
|
||||
for _, phase := range availablePhases {
|
||||
if err := copyPhaseFile(oldAppName, newAppName, phase); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := common.PropertyClone("docker-options", oldAppName, newAppName); err != nil {
|
||||
return err
|
||||
}
|
||||
return common.PropertyDestroy("docker-options", oldAppName)
|
||||
}
|
||||
|
||||
// TriggerPostDelete removes the docker option properties for an app and
|
||||
// cleans up any leftover migrated legacy files.
|
||||
func TriggerPostDelete(appName string) error {
|
||||
if err := common.PropertyDestroy("docker-options", appName); err != nil {
|
||||
return err
|
||||
}
|
||||
removeMigratedLegacyFiles(appName)
|
||||
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
|
||||
}
|
||||
// TriggerDockerArgs implements the legacy docker-args-{build,deploy,run}
|
||||
// triggers. It echoes stdin verbatim, then appends the default-scope options
|
||||
// for the given phase after applying image-source-type filtering.
|
||||
func TriggerDockerArgs(phase, appName, imageSourceType, processType string) error {
|
||||
stdin, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stdout.Write(stdin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options, err := GetDockerOptionsForProcessPhase(appName, DefaultProcessType, phase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
emitFilteredOptions(phase, imageSourceType, options)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerDockerArgsProcessDeploy implements docker-args-process-deploy. It
|
||||
// echoes stdin verbatim, then appends the deploy-phase options for the
|
||||
// process-specific scope (filtered by image source type). Default-scope
|
||||
// options are emitted by TriggerDockerArgs (docker-args-deploy), which the
|
||||
// scheduler invokes alongside the per-process trigger.
|
||||
func TriggerDockerArgsProcessDeploy(appName, imageSourceType, processType string) error {
|
||||
stdin, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stdout.Write(stdin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if processType == "" || processType == DefaultProcessType {
|
||||
return nil
|
||||
}
|
||||
|
||||
options, err := GetDockerOptionsForProcessPhase(appName, processType, "deploy")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
emitFilteredOptions("deploy", imageSourceType, options)
|
||||
return nil
|
||||
}
|
||||
|
||||
// emitFilteredOptions writes filtered docker options to stdout, prefixed with
|
||||
// a single space so concatenation with prior output stays well-formed. It
|
||||
// reproduces the filtering historically performed by the bash docker-args-*
|
||||
// triggers.
|
||||
func emitFilteredOptions(phase, imageSourceType string, options []string) {
|
||||
for _, option := range options {
|
||||
option = strings.TrimSpace(option)
|
||||
if option == "" || strings.HasPrefix(option, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(option, "--restart") {
|
||||
if phase == "deploy" {
|
||||
fmt.Printf(" %s", option)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch imageSourceType {
|
||||
case "dockerfile", "nixpacks", "railpack":
|
||||
if hasAnyPrefix(option, "--link", "-v", "--volume") {
|
||||
continue
|
||||
}
|
||||
case "herokuish":
|
||||
if hasAnyPrefix(option, "--file", "--build-args") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf(" %s", option)
|
||||
}
|
||||
}
|
||||
|
||||
func hasAnyPrefix(s string, prefixes ...string) bool {
|
||||
for _, prefix := range prefixes {
|
||||
if s == prefix || strings.HasPrefix(s, prefix+"=") || strings.HasPrefix(s, prefix+" ") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
266
tests/unit/docker-options-2.bats
Normal file
266
tests/unit/docker-options-2.bats
Normal file
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() {
|
||||
global_setup
|
||||
create_app
|
||||
}
|
||||
|
||||
teardown() {
|
||||
destroy_app
|
||||
global_teardown
|
||||
}
|
||||
|
||||
@test "(docker-options:add) --process scopes option to one process type" {
|
||||
run /bin/bash -c "dokku docker-options:add --process web $TEST_APP deploy '-p 8080:5000'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $TEST_APP --process web --phase deploy"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "-p 8080:5000"
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $TEST_APP --phase deploy"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output_contains "-p 8080:5000" 0
|
||||
}
|
||||
|
||||
@test "(docker-options:add) multiple --process flags add to each process" {
|
||||
run /bin/bash -c "dokku docker-options:add --process web --process worker $TEST_APP deploy '-v /shared:/shared'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $TEST_APP --process web --phase deploy"
|
||||
echo "output: $output"
|
||||
assert_output "-v /shared:/shared"
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $TEST_APP --process worker --phase deploy"
|
||||
echo "output: $output"
|
||||
assert_output "-v /shared:/shared"
|
||||
}
|
||||
|
||||
@test "(docker-options:add) without --process keeps default-scope behaviour" {
|
||||
run /bin/bash -c "dokku docker-options:add $TEST_APP deploy '-v /tmp/shared:/shared'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $TEST_APP --phase deploy"
|
||||
echo "output: $output"
|
||||
assert_output_contains "-v /tmp/shared:/shared"
|
||||
|
||||
run /bin/bash -c "dokku docker-options:report $TEST_APP --docker-options-deploy"
|
||||
echo "output: $output"
|
||||
assert_output_contains "-v /tmp/shared:/shared"
|
||||
}
|
||||
|
||||
@test "(docker-options:add) rejects --process for non-deploy phases" {
|
||||
run /bin/bash -c "dokku docker-options:add --process web $TEST_APP build '--shm-size 256m'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "deploy phase"
|
||||
|
||||
run /bin/bash -c "dokku docker-options:add --process web $TEST_APP run '--shm-size 256m'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
|
||||
run /bin/bash -c "dokku docker-options:add --process web $TEST_APP build,deploy '--shm-size 256m'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "(docker-options:add) rejects reserved _default_ process name" {
|
||||
run /bin/bash -c "dokku docker-options:add --process _default_ $TEST_APP deploy '-v /tmp:/tmp'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "_default_"
|
||||
}
|
||||
|
||||
@test "(docker-options:add) warns on process types missing from Procfile" {
|
||||
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:add --process nonexistent $TEST_APP deploy '-v /tmp:/tmp'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output_contains "nonexistent"
|
||||
}
|
||||
|
||||
@test "(docker-options:remove) --process removes from a single process" {
|
||||
run /bin/bash -c "dokku docker-options:add --process web $TEST_APP deploy '-p 8080:5000'"
|
||||
echo "output: $output"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:add --process worker $TEST_APP deploy '-p 9000:9000'"
|
||||
echo "output: $output"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:remove --process web $TEST_APP deploy '-p 8080:5000'"
|
||||
echo "output: $output"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $TEST_APP --process web --phase deploy"
|
||||
echo "output: $output"
|
||||
assert_output ""
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $TEST_APP --process worker --phase deploy"
|
||||
echo "output: $output"
|
||||
assert_output "-p 9000:9000"
|
||||
}
|
||||
|
||||
@test "(docker-options:clear) --process clears only that process" {
|
||||
run /bin/bash -c "dokku docker-options:add --process web $TEST_APP deploy '-p 8080:5000'"
|
||||
echo "output: $output"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:add --process worker $TEST_APP deploy '-p 9000:9000'"
|
||||
echo "output: $output"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:clear --process web $TEST_APP deploy"
|
||||
echo "output: $output"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $TEST_APP --process web --phase deploy"
|
||||
echo "output: $output"
|
||||
assert_output ""
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $TEST_APP --process worker --phase deploy"
|
||||
echo "output: $output"
|
||||
assert_output "-p 9000:9000"
|
||||
}
|
||||
|
||||
@test "(docker-options:list) --phase is required" {
|
||||
run /bin/bash -c "dokku docker-options:list $TEST_APP"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "phase"
|
||||
}
|
||||
|
||||
@test "(docker-options:report) exposes dynamic per-process keys" {
|
||||
run /bin/bash -c "dokku docker-options:add --process web $TEST_APP deploy '-p 8080:5000'"
|
||||
echo "output: $output"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:report $TEST_APP --docker-options-deploy.web"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output_contains "-p 8080:5000"
|
||||
|
||||
run /bin/bash -c "dokku docker-options:report $TEST_APP"
|
||||
echo "output: $output"
|
||||
assert_success
|
||||
assert_output_contains "Docker options deploy web"
|
||||
assert_output_contains "-p 8080:5000"
|
||||
}
|
||||
|
||||
@test "(docker-options:report) --format json carries process-scoped options" {
|
||||
run /bin/bash -c "dokku docker-options:add --process web $TEST_APP deploy '-p 8080:5000'"
|
||||
echo "output: $output"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:add $TEST_APP deploy '-v /logs:/logs'"
|
||||
echo "output: $output"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:report $TEST_APP --format json"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output_contains '"docker-options-deploy.web"'
|
||||
assert_output_contains "-p 8080:5000"
|
||||
assert_output_contains '"docker-options-deploy"'
|
||||
assert_output_contains "-v /logs:/logs"
|
||||
}
|
||||
|
||||
@test "(docker-options) clone copies default and per-process options" {
|
||||
local CLONE_APP="${TEST_APP}-clone"
|
||||
run /bin/bash -c "dokku docker-options:add --process web $TEST_APP deploy '-p 8080:5000'"
|
||||
assert_success
|
||||
run /bin/bash -c "dokku docker-options:add $TEST_APP deploy '-v /logs:/logs'"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku apps:clone --skip-deploy $TEST_APP $CLONE_APP"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $CLONE_APP --process web --phase deploy"
|
||||
echo "output: $output"
|
||||
assert_output "-p 8080:5000"
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $CLONE_APP --phase deploy"
|
||||
echo "output: $output"
|
||||
assert_output_contains "-v /logs:/logs"
|
||||
|
||||
dokku --force apps:destroy "$CLONE_APP" || true
|
||||
}
|
||||
|
||||
@test "(docker-options) rename moves default and per-process options" {
|
||||
local RENAMED_APP="${TEST_APP}-renamed"
|
||||
run /bin/bash -c "dokku docker-options:add --process web $TEST_APP deploy '-p 8080:5000'"
|
||||
assert_success
|
||||
run /bin/bash -c "dokku docker-options:add $TEST_APP deploy '-v /logs:/logs'"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku apps:rename $TEST_APP $RENAMED_APP"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $RENAMED_APP --process web --phase deploy"
|
||||
assert_output "-p 8080:5000"
|
||||
|
||||
run /bin/bash -c "dokku docker-options:list $RENAMED_APP --phase deploy"
|
||||
assert_output_contains "-v /logs:/logs"
|
||||
|
||||
[[ ! -d "/var/lib/dokku/config/docker-options/$TEST_APP" ]]
|
||||
|
||||
TEST_APP="$RENAMED_APP"
|
||||
}
|
||||
|
||||
@test "(docker-options) deploy with web-only port mapping does not break worker" {
|
||||
run /bin/bash -c "dokku docker-options:add --process web $TEST_APP deploy '-p 8080:5000'"
|
||||
echo "output: $output"
|
||||
assert_success
|
||||
|
||||
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP web_worker_callback
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output_contains "App container failed to start" 0
|
||||
assert_output_contains "all ports are allocated" 0
|
||||
|
||||
run /bin/bash -c "dokku ps:report $TEST_APP --ps-status-web-1"
|
||||
echo "output: $output"
|
||||
assert_output_contains "running"
|
||||
|
||||
run /bin/bash -c "dokku ps:report $TEST_APP --ps-status-worker-1"
|
||||
echo "output: $output"
|
||||
assert_output_contains "running"
|
||||
}
|
||||
|
||||
web_worker_callback() {
|
||||
local APP="$1"
|
||||
local APP_REPO_DIR="$2"
|
||||
cat >"$APP_REPO_DIR/Procfile" <<EOF
|
||||
web: python3 -u web.py
|
||||
worker: python3 -u worker.py
|
||||
EOF
|
||||
}
|
||||
@@ -264,11 +264,7 @@ teardown() {
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
run /bin/bash -c "echo '-v /tmp' >> $DOKKU_ROOT/$TEST_APP/DOCKER_OPTIONS_DEPLOY"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
run /bin/bash -c "echo '# comment' >> $DOKKU_ROOT/$TEST_APP/DOCKER_OPTIONS_DEPLOY"
|
||||
run /bin/bash -c "dokku docker-options:add $TEST_APP deploy \"-v /tmp\""
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
@@ -295,11 +291,7 @@ teardown() {
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
run /bin/bash -c "echo '-v /tmp' >> $DOKKU_ROOT/$TEST_APP/DOCKER_OPTIONS_DEPLOY"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
run /bin/bash -c "echo '# comment' >> $DOKKU_ROOT/$TEST_APP/DOCKER_OPTIONS_DEPLOY"
|
||||
run /bin/bash -c "dokku docker-options:add $TEST_APP deploy \"-v /tmp\""
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
Reference in New Issue
Block a user