fix: respect quoting in procfile commands
Procfile commands for the `docker-local` scheduler were split differently on deploy and on `dokku run`. Deploys failed on escaped quotes, and `dokku run` ignored quotes entirely. Both now split commands using shell quoting rules, and commands containing bare shell operators fail with a clear error, matching the `k3s` scheduler.
This commit is contained in:
@@ -52,6 +52,8 @@ console: bundle exec racksh
|
||||
dokku run my-app console
|
||||
```
|
||||
|
||||
The Procfile command is split into arguments using the same shell quoting rules used when deploying the app. See the [Procfile documentation](/docs/processes/process-management.md#procfile) for more details.
|
||||
|
||||
#### Specifying container labels
|
||||
|
||||
Containers may have specific labels attached. In order to avoid issues with dokku internals, do not use any labels beginning with either `com.dokku` or `org.label-schema`.
|
||||
|
||||
@@ -179,6 +179,12 @@ importantworker: env QUEUE=important bundle exec rake resque:work
|
||||
|
||||
If the app build declares an `ENTRYPOINT`, the command defined in the `Procfile` is passed as an argument to that entrypoint. This is the case for all Dockerfile-based, Docker Image, and Cloud Native Buildpack deployments.
|
||||
|
||||
When deploying via the `docker-local` scheduler, for all builders other than Herokuish, the command is split into arguments using shell quoting rules, but is not otherwise interpreted by a shell. Environment variable references such as `$PORT` are substituted before the command is split. Single quotes, double quotes, and backslash escapes then group and escape arguments, and any other special characters are passed to the container verbatim. Shell operators such as `&&`, `|`, `;`, and redirections are not supported and will fail the deploy, and a trailing `#` comment is ignored. If shell features are required, invoke a shell explicitly:
|
||||
|
||||
```Procfile
|
||||
web: sh -c "bin/migrate && exec bin/server --port $PORT"
|
||||
```
|
||||
|
||||
The `web` process type holds some significance in that it is the only process type that is automatically scaled to `1` on the initial application deploy. See the [web process scaling documentation](/docs/processes/process-management.md#the-web-process) for more details around scaling individual processes.
|
||||
|
||||
See the [Procfile location documentation](/docs/processes/process-management.md#changing-the-procfile-location) for more information on where to place your `Procfile` file.
|
||||
|
||||
@@ -251,6 +251,12 @@ fn-docker-args-split() {
|
||||
printf "%s" "$DOCKER_ARGS" | xargs -r printf "%s\0"
|
||||
}
|
||||
|
||||
fn-split-command() {
|
||||
declare desc="tokenizes a command string into NUL-delimited fields using shell quoting rules without shell expansion"
|
||||
declare COMMAND="$1"
|
||||
"$PLUGIN_CORE_AVAILABLE_PATH/common/common" split-command -- "$COMMAND"
|
||||
}
|
||||
|
||||
is_valid_app_name() {
|
||||
declare desc="verify that the app name matches naming restrictions"
|
||||
local APP="$1"
|
||||
|
||||
@@ -13,6 +13,7 @@ require (
|
||||
github.com/spf13/pflag v1.0.10
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/sync v0.23.0
|
||||
mvdan.cc/sh/v3 v3.14.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -2,6 +2,8 @@ github.com/alexellis/go-execute/v2 v2.2.1 h1:4Ye3jiCKQarstODOEmqDSRCqxMHLkC92Bhs
|
||||
github.com/alexellis/go-execute/v2 v2.2.1/go.mod h1:FMdRnUTiFAmYXcv23txrp3VYZfLo24nMpiIneWgKHTQ=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
|
||||
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -11,6 +13,10 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
@@ -27,6 +33,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
|
||||
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
|
||||
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
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=
|
||||
@@ -47,3 +55,5 @@ golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
mvdan.cc/sh/v3 v3.14.1 h1:bXkhQWNHCs0KZEChF8hYS6FC+T2N9mUZLbQv9blditI=
|
||||
mvdan.cc/sh/v3 v3.14.1/go.mod h1:syYCoFET8w9tvevxiXUtY8/ICrU+l26jHmhJDra3Vwo=
|
||||
|
||||
91
plugins/common/shellwords/shellwords.go
Normal file
91
plugins/common/shellwords/shellwords.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package shellwords
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
)
|
||||
|
||||
// Split splits input into shell words using the parser directly, so quotes
|
||||
// delimit words and are stripped from the returned value, but nothing is
|
||||
// expanded. Parameter expansions, command substitutions, and other
|
||||
// metacharacters are preserved verbatim. Malformed input, such as an unbalanced
|
||||
// quote, returns the parser error.
|
||||
func Split(input string) ([]string, error) {
|
||||
fields, _, err := SplitWithEnd(input)
|
||||
return fields, err
|
||||
}
|
||||
|
||||
// SplitWithEnd is Split plus the byte offset just past the last word the
|
||||
// parser consumed. Callers that must not lose data compare the offset against
|
||||
// the length of the input to detect a tail the parser skipped, such as a shell
|
||||
// comment.
|
||||
func SplitWithEnd(input string) ([]string, int, error) {
|
||||
parser := syntax.NewParser()
|
||||
var fields []string
|
||||
end := 0
|
||||
for word, err := range parser.WordsSeq(strings.NewReader(input)) {
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
fields = append(fields, literalWordValue(input, word.Parts))
|
||||
end = int(word.End().Offset())
|
||||
}
|
||||
return fields, end, nil
|
||||
}
|
||||
|
||||
// literalWordValue reconstructs the unquoted, unexpanded value of a shell word.
|
||||
// Literal and single-quoted parts contribute their literal text; double-quoted
|
||||
// parts have their surrounding quotes dropped while their contents stay
|
||||
// literal; every other part - parameter expansions, command substitutions,
|
||||
// arithmetic expansions - contributes its original source text unchanged.
|
||||
// Backslash escapes are removed the same way the shell removes them during
|
||||
// quote removal so a value such as `Host(\`app\`)` round-trips to `Host(`app`)`.
|
||||
func literalWordValue(input string, parts []syntax.WordPart) string {
|
||||
var sb strings.Builder
|
||||
for _, part := range parts {
|
||||
switch p := part.(type) {
|
||||
case *syntax.Lit:
|
||||
sb.WriteString(unquoteBackslashes(p.Value, false))
|
||||
case *syntax.SglQuoted:
|
||||
sb.WriteString(p.Value)
|
||||
case *syntax.DblQuoted:
|
||||
for _, inner := range p.Parts {
|
||||
if lit, ok := inner.(*syntax.Lit); ok {
|
||||
sb.WriteString(unquoteBackslashes(lit.Value, true))
|
||||
continue
|
||||
}
|
||||
sb.WriteString(input[inner.Pos().Offset():inner.End().Offset()])
|
||||
}
|
||||
default:
|
||||
sb.WriteString(input[part.Pos().Offset():part.End().Offset()])
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// unquoteBackslashes removes backslashes the way the shell does during quote
|
||||
// removal. Inside double quotes only \$, \`, \", \\, and an escaped newline
|
||||
// lose their backslash; unquoted, a backslash escapes any following character.
|
||||
func unquoteBackslashes(s string, inDblQuotes bool) string {
|
||||
if !strings.Contains(s, "\\") {
|
||||
return s
|
||||
}
|
||||
var sb strings.Builder
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\\' && i+1 < len(s) {
|
||||
next := s[i+1]
|
||||
if next == '\n' {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if !inDblQuotes || next == '$' || next == '`' || next == '"' || next == '\\' {
|
||||
sb.WriteByte(next)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
sb.WriteByte(s[i])
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
107
plugins/common/shellwords/shellwords_test.go
Normal file
107
plugins/common/shellwords/shellwords_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package shellwords
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
},
|
||||
{
|
||||
name: "plain words",
|
||||
input: "python -m http.server 5000",
|
||||
want: []string{"python", "-m", "http.server", "5000"},
|
||||
},
|
||||
{
|
||||
name: "double quoted word",
|
||||
input: `sh -c "echo hi"`,
|
||||
want: []string{"sh", "-c", "echo hi"},
|
||||
},
|
||||
{
|
||||
name: "single quoted word",
|
||||
input: `sh -c 'echo hi'`,
|
||||
want: []string{"sh", "-c", "echo hi"},
|
||||
},
|
||||
{
|
||||
name: "escaped quotes inside double quotes",
|
||||
input: `sh -c "echo \"hi\""`,
|
||||
want: []string{"sh", "-c", `echo "hi"`},
|
||||
},
|
||||
{
|
||||
name: "escaped single quote outside quotes",
|
||||
input: `echo it\'s`,
|
||||
want: []string{"echo", "it's"},
|
||||
},
|
||||
{
|
||||
name: "parameter expansion kept verbatim",
|
||||
input: `sh -c "echo $PORT"`,
|
||||
want: []string{"sh", "-c", "echo $PORT"},
|
||||
},
|
||||
{
|
||||
name: "command substitution kept verbatim",
|
||||
input: `echo $(date)`,
|
||||
want: []string{"echo", "$(date)"},
|
||||
},
|
||||
{
|
||||
name: "backticks kept verbatim",
|
||||
input: "echo `date`",
|
||||
want: []string{"echo", "`date`"},
|
||||
},
|
||||
{
|
||||
name: "escaped backticks inside double quotes",
|
||||
input: "echo \"\\`date\\`\"",
|
||||
want: []string{"echo", "`date`"},
|
||||
},
|
||||
{
|
||||
name: "unterminated quote",
|
||||
input: `sh -c "echo hi`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "bare operator",
|
||||
input: `a && b`,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := Split(tc.input)
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("Split(%q) expected error, got %q", tc.input, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Split(%q) unexpected error: %v", tc.input, err)
|
||||
}
|
||||
if !slices.Equal(got, tc.want) {
|
||||
t.Fatalf("Split(%q) = %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitWithEndReportsSkippedComment(t *testing.T) {
|
||||
input := "a # comment"
|
||||
fields, end, err := SplitWithEnd(input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !slices.Equal(fields, []string{"a"}) {
|
||||
t.Fatalf("fields = %q, want [a]", fields)
|
||||
}
|
||||
if end != 1 {
|
||||
t.Fatalf("end = %d, want 1", end)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
"github.com/dokku/dokku/plugins/common/shellwords"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
@@ -92,6 +93,16 @@ func main() {
|
||||
case "is-valid-app-name-old":
|
||||
appName := flag.Arg(1)
|
||||
err = common.IsValidAppNameOld(appName)
|
||||
case "split-command":
|
||||
var fields []string
|
||||
fields, err = shellwords.Split(flag.Arg(1))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Unable to parse command %q: %w", flag.Arg(1), err)
|
||||
break
|
||||
}
|
||||
for _, field := range fields {
|
||||
fmt.Printf("%s\x00", field)
|
||||
}
|
||||
case "verify-app-name":
|
||||
appName := flag.Arg(1)
|
||||
err = common.VerifyAppName(appName)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
"github.com/dokku/dokku/plugins/common/shellwords"
|
||||
)
|
||||
|
||||
// DefaultProcessType is the sentinel process-type key used for options that
|
||||
@@ -32,7 +32,7 @@ func SplitOptionString(input string) (options []string, processes []string, err
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
fields, err := literalFields(input)
|
||||
fields, err := shellwords.Split(input)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Unable to parse docker option: %s", err.Error())
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func groupOptionTokens(fields []string) [][]string {
|
||||
// stops before consuming all of it - a line whose tail begins with an unquoted
|
||||
// `#` reads as a shell comment, and dropping it would lose stored data.
|
||||
func canonicalOptionsFromLine(line string) []string {
|
||||
fields, end, err := literalFieldsWithEnd(line)
|
||||
fields, end, err := shellwords.SplitWithEnd(line)
|
||||
if err != nil || end < len(strings.TrimRight(line, " \t")) {
|
||||
return []string{line}
|
||||
}
|
||||
@@ -118,12 +118,12 @@ func optionsEqual(stored string, option string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
storedFields, err := literalFields(stored)
|
||||
storedFields, err := shellwords.Split(stored)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
optionFields, err := literalFields(option)
|
||||
optionFields, err := shellwords.Split(option)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -131,90 +131,6 @@ func optionsEqual(stored string, option string) bool {
|
||||
return slices.Equal(storedFields, optionFields)
|
||||
}
|
||||
|
||||
// literalFields splits input into shell words using the parser directly, so
|
||||
// quotes delimit words and are stripped from the stored value, but nothing is
|
||||
// expanded. Parameter expansions, command substitutions, and other
|
||||
// metacharacters are preserved verbatim. Malformed input, such as an unbalanced
|
||||
// quote, returns the parser error.
|
||||
func literalFields(input string) ([]string, error) {
|
||||
fields, _, err := literalFieldsWithEnd(input)
|
||||
return fields, err
|
||||
}
|
||||
|
||||
// literalFieldsWithEnd is literalFields plus the byte offset just past the last
|
||||
// word the parser consumed. Callers that must not lose data compare the offset
|
||||
// against the length of the input to detect a tail the parser skipped, such as
|
||||
// a shell comment.
|
||||
func literalFieldsWithEnd(input string) ([]string, int, error) {
|
||||
parser := syntax.NewParser()
|
||||
var fields []string
|
||||
end := 0
|
||||
for word, err := range parser.WordsSeq(strings.NewReader(input)) {
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
fields = append(fields, literalWordValue(input, word.Parts))
|
||||
end = int(word.End().Offset())
|
||||
}
|
||||
return fields, end, nil
|
||||
}
|
||||
|
||||
// literalWordValue reconstructs the unquoted, unexpanded value of a shell word.
|
||||
// Literal and single-quoted parts contribute their literal text; double-quoted
|
||||
// parts have their surrounding quotes dropped while their contents stay
|
||||
// literal; every other part - parameter expansions, command substitutions,
|
||||
// arithmetic expansions - contributes its original source text unchanged.
|
||||
// Backslash escapes are removed the same way the shell removes them during
|
||||
// quote removal so a value such as `Host(\`app\`)` round-trips to `Host(`app`)`.
|
||||
func literalWordValue(input string, parts []syntax.WordPart) string {
|
||||
var sb strings.Builder
|
||||
for _, part := range parts {
|
||||
switch p := part.(type) {
|
||||
case *syntax.Lit:
|
||||
sb.WriteString(unquoteBackslashes(p.Value, false))
|
||||
case *syntax.SglQuoted:
|
||||
sb.WriteString(p.Value)
|
||||
case *syntax.DblQuoted:
|
||||
for _, inner := range p.Parts {
|
||||
if lit, ok := inner.(*syntax.Lit); ok {
|
||||
sb.WriteString(unquoteBackslashes(lit.Value, true))
|
||||
continue
|
||||
}
|
||||
sb.WriteString(input[inner.Pos().Offset():inner.End().Offset()])
|
||||
}
|
||||
default:
|
||||
sb.WriteString(input[part.Pos().Offset():part.End().Offset()])
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// unquoteBackslashes removes backslashes the way the shell does during quote
|
||||
// removal. Inside double quotes only \$, \`, \", \\, and an escaped newline
|
||||
// lose their backslash; unquoted, a backslash escapes any following character.
|
||||
func unquoteBackslashes(s string, inDblQuotes bool) string {
|
||||
if !strings.Contains(s, "\\") {
|
||||
return s
|
||||
}
|
||||
var sb strings.Builder
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\\' && i+1 < len(s) {
|
||||
next := s[i+1]
|
||||
if next == '\n' {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if !inDblQuotes || next == '$' || next == '`' || next == '"' || next == '\\' {
|
||||
sb.WriteByte(next)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
sb.WriteByte(s[i])
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// isFlagToken reports whether tok looks like a CLI flag (long or short) rather
|
||||
// than a value. Treating any token that begins with `-` and has more than one
|
||||
// character as a flag matches docker's flag conventions and avoids the need
|
||||
|
||||
@@ -5,7 +5,6 @@ go 1.26.2
|
||||
require (
|
||||
github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000
|
||||
github.com/spf13/pflag v1.0.10
|
||||
mvdan.cc/sh/v3 v3.14.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -25,6 +24,7 @@ require (
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
mvdan.cc/sh/v3 v3.14.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/dokku/dokku/plugins/common => ../common
|
||||
|
||||
@@ -65,12 +65,15 @@ main() {
|
||||
|
||||
START_CMD=$(fn-scheduler-docker-local-extract-start-cmd "$APP" "$PROC_TYPE" "$START_CMD" "$DOKKU_HEROKUISH" "$DOKKU_PORT")
|
||||
DOCKER_ARGS+=" $IMAGE"
|
||||
DOCKER_ARGS+=" $START_CMD"
|
||||
fn-split-command "$START_CMD" >/dev/null
|
||||
|
||||
declare -a ARG_ARRAY=()
|
||||
while IFS= read -r -d '' arg; do
|
||||
ARG_ARRAY+=("$arg")
|
||||
done < <(fn-docker-args-split "$DOCKER_ARGS")
|
||||
while IFS= read -r -d '' arg; do
|
||||
ARG_ARRAY+=("$arg")
|
||||
done < <(fn-split-command "$START_CMD")
|
||||
cid=$(fn-scheduler-docker-local-start-app-container "$APP" "${ARG_ARRAY[@]}")
|
||||
|
||||
plugn trigger post-container-create "app" "$cid" "$APP" "deploy" "$PROC_TYPE"
|
||||
|
||||
@@ -111,7 +111,11 @@ trigger-scheduler-docker-local-scheduler-run() {
|
||||
if [[ -n "$PROC_CMD" ]]; then
|
||||
dokku_log_info1_quiet "Found '$POTENTIAL_PROCFILE_KEY' in Procfile, running that command"
|
||||
DOCKER_ARGS+=" --label=com.dokku.process-type=$POTENTIAL_PROCFILE_KEY"
|
||||
read -ra proc_array <<<"$PROC_CMD"
|
||||
fn-split-command "$PROC_CMD" >/dev/null
|
||||
local -a proc_array=()
|
||||
while IFS= read -r -d '' arg; do
|
||||
proc_array+=("$arg")
|
||||
done < <(fn-split-command "$PROC_CMD")
|
||||
set -- "${proc_array[@]}" "${@:2}"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -483,3 +483,71 @@ teardown() {
|
||||
echo "status: $status"
|
||||
assert_output "running"
|
||||
}
|
||||
|
||||
@test "(scheduler-docker-local) procfile quoted arguments on deploy" {
|
||||
run create_app
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP quoted_procfile_callback
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "docker inspect --format '{{json .Config.Cmd}}' $TEST_APP.web.1"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output '["python3","-u","web.py","double quoted","single quoted","escaped \"quote\"","literal`tick`","5000"]'
|
||||
}
|
||||
|
||||
@test "(scheduler-docker-local) procfile quoted arguments on run" {
|
||||
run create_app
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP quoted_procfile_callback
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku run $TEST_APP args"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output_contains "['double quoted', 'single quoted', 'escaped \"quote\"', 'literal\`tick\`', '5000']"
|
||||
}
|
||||
|
||||
@test "(scheduler-docker-local) procfile with shell operators fails deploy" {
|
||||
run create_app
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP operator_procfile_callback
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "Unable to parse command"
|
||||
}
|
||||
|
||||
quoted_procfile_callback() {
|
||||
local APP="$1"
|
||||
local APP_REPO_DIR="$2"
|
||||
mv "$APP_REPO_DIR/dockerfile.Dockerfile" "$APP_REPO_DIR/Dockerfile"
|
||||
cat >"$APP_REPO_DIR/Procfile" <<'PROCFILE'
|
||||
web: python3 -u web.py "double quoted" 'single quoted' "escaped \"quote\"" literal\`tick\` "$PORT"
|
||||
args: python3 -c "import sys; print(sys.argv[1:])" "double quoted" 'single quoted' "escaped \"quote\"" literal\`tick\` "$PORT"
|
||||
PROCFILE
|
||||
}
|
||||
|
||||
operator_procfile_callback() {
|
||||
local APP="$1"
|
||||
local APP_REPO_DIR="$2"
|
||||
mv "$APP_REPO_DIR/dockerfile.Dockerfile" "$APP_REPO_DIR/Dockerfile"
|
||||
cat >"$APP_REPO_DIR/Procfile" <<'PROCFILE'
|
||||
web: python3 -u web.py && echo done
|
||||
PROCFILE
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user