feat: scope docker-options to specific procfile processes
Adds a `--process` flag (repeatable) to docker-options:add/remove/clear/list and the new docker-options:list subcommand for querying a single process+phase pair. Process scoping is supported only for the deploy phase since build runs once per app and run covers ad-hoc commands and cron tasks where no Procfile process type is available. Storage moves from `$DOKKU_ROOT/$APP/DOCKER_OPTIONS_*` files to property lists under `/var/lib/dokku/config/docker-options/$APP/{processType}.{phase}`, with `_default_` as the sentinel for app-wide options. The install trigger migrates pre-existing DOCKER_OPTIONS_* files into property lists once and renames them to `.migrated`; a global marker makes re-runs strictly no-op. The legacy docker-args-{build,deploy,run} bash triggers are reimplemented in Go alongside a new docker-args-process-deploy trigger that surfaces per-process options to the scheduler. The :report command exposes one dynamic flag per configured `process.deploy` pair (e.g. `--docker-options-deploy.web`) and supports `--format json`. There is no `--global` flag; omitting `--process` keeps the historical default behaviour, since `--global` elsewhere in dokku means "across all apps". Closes #2441.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user