Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions plugins/apache/process-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "0.5"

processes:
apache:
command: "echo \"Apache starting on port $HTTPD_PORT\ http://localhost:$HTTPD_PORT\" && apachectl start -f $HTTPD_CONFDIR/httpd.conf -D FOREGROUND"
command: "echo \"Apache starting on port $HTTPD_PORT\ http://localhost:$HTTPD_PORT\" && apachectl start -f \"$HTTPD_CONFDIR/httpd.conf\" -D FOREGROUND"
availability:
restart: on_failure
max_restarts: 5
Expand All @@ -12,10 +12,10 @@ processes:
apache-access:
condition: process_started
apache-error:
command: "tail -f $HTTPD_ERROR_LOG_FILE"
command: "tail -f \"$HTTPD_ERROR_LOG_FILE\""
availability:
restart: "always"
apache-access:
command: "tail -f $HTTPD_ACCESS_LOG_FILE"
command: "tail -f \"$HTTPD_ACCESS_LOG_FILE\""
availability:
restart: "always"
2 changes: 1 addition & 1 deletion plugins/apacheHttpd.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apache",
"version": "0.0.2",
"version": "0.0.3",
"description": "If you with to edit the config file, please copy it out of the .devbox directory.",
"env": {
"HTTPD_DEVBOX_CONFIG_DIR": "{{ .DevboxProjectDir }}",
Expand Down
2 changes: 1 addition & 1 deletion plugins/caddy.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "caddy",
"version": "0.0.3",
"version": "0.0.4",
"description": "You can customize the config used by the caddy service by modifying the Caddyfile in devbox.d/caddy, or by changing the CADDY_CONFIG environment variable to point to a custom config. The custom config must be either JSON or Caddyfile format.",
"env": {
"CADDY_CONFIG": "{{ .DevboxDir }}/Caddyfile",
Expand Down
2 changes: 1 addition & 1 deletion plugins/caddy/process-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "0.6"

processes:
caddy:
command: "caddy run --config=$CADDY_CONFIG"
command: "caddy run --config=\"$CADDY_CONFIG\""
availability:
restart: on_failure
max_restarts: 5
2 changes: 1 addition & 1 deletion plugins/php.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "php",
"version": "0.0.3",
"version": "0.0.4",
"description": "PHP is compiled with default extensions. If you would like to use non-default extensions you can add them with devbox add php81Extensions.{extension} . For example, for the memcache extension you can do `devbox add php81Extensions.memcached`.",
"packages": [
"path:{{ .Virtenv }}/flake",
Expand Down
2 changes: 1 addition & 1 deletion plugins/php/process-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "0.5"

processes:
php-fpm:
command: "php-fpm -y {{ .DevboxDir }}/php-fpm.conf --nodaemonize"
command: "php-fpm -y \"{{ .DevboxDir }}/php-fpm.conf\" --nodaemonize"
availability:
restart: "always"

2 changes: 1 addition & 1 deletion plugins/redis.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redis",
"version": "0.0.2",
"version": "0.0.3",
"description": "Running `devbox services start redis` will start redis as a daemon in the background. \n\nYou can manually start Redis in the foreground by running `redis-server $REDIS_CONF --port $REDIS_PORT`. \n\nLogs, pidfile, and data dumps are stored in `.devbox/virtenv/redis`. You can change this by modifying the `dir` directive in `devbox.d/redis/redis.conf`",
"env": {
"REDIS_PORT": "6379",
Expand Down
2 changes: 1 addition & 1 deletion plugins/redis/process-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "0.5"

processes:
redis:
command: "redis-server $REDIS_CONF --port $REDIS_PORT"
command: "redis-server \"$REDIS_CONF\" --port $REDIS_PORT"
availability:
restart: on_failure
max_restarts: 5
176 changes: 176 additions & 0 deletions plugins/service_command_quoting_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package plugins

import (
"io/fs"
"strings"
"testing"

"gopkg.in/yaml.v3"
)

// TestServiceCommandTemplatePathsAreQuoted guards against a regression where a
// builtin plugin's process-compose service command references a templated
// filesystem path (for example "{{ .DevboxDir }}/php-fpm.conf") without
// wrapping it in double quotes. Because those templates expand to the
// project's absolute path, an unquoted reference is word-split by the shell
// whenever the project directory contains a space, and the service fails to
// start. See jetify-com/devbox#2631.
func TestServiceCommandTemplatePathsAreQuoted(t *testing.T) {
files := processComposeFiles(t)

for _, file := range files {
t.Run(file, func(t *testing.T) {
for _, command := range serviceCommands(t, file) {
quoted := shellQuotedPositions(command)
for _, idx := range templateIndices(command) {
if !quoted[idx] {
t.Errorf(
"%s: service command %q has an unquoted templated path; "+
"wrap it in double quotes so it survives project paths with spaces",
file, command,
)
}
}
}
})
}
}

// TestServiceCommandEnvPathsAreQuoted guards against the same regression for
// service commands that reference a path through an environment variable (for
// example "$CADDY_CONFIG", which expands to "{{ .DevboxDir }}/Caddyfile").
// Only variables that are known to hold an absolute project path are checked;
// variables such as ports do not need quoting. See jetify-com/devbox#2631.
func TestServiceCommandEnvPathsAreQuoted(t *testing.T) {
// pathEnvVars maps a plugin's process-compose.yaml to the environment
// variables it references that expand to an absolute project path and so
// must be quoted wherever they appear in a service command.
pathEnvVars := map[string][]string{
"apache/process-compose.yaml": {
"HTTPD_CONFDIR",
"HTTPD_ERROR_LOG_FILE",
"HTTPD_ACCESS_LOG_FILE",
},
"caddy/process-compose.yaml": {"CADDY_CONFIG"},
"postgresql/process-compose.yaml": {"PGHOST"},
"redis/process-compose.yaml": {"REDIS_CONF"},
"valkey/process-compose.yaml": {"VALKEY_CONF"},
}

for file, vars := range pathEnvVars {
t.Run(file, func(t *testing.T) {
commands := serviceCommands(t, file)
for _, command := range commands {
quoted := shellQuotedPositions(command)
for _, name := range vars {
for _, idx := range envVarIndices(command, name) {
if !quoted[idx] {
t.Errorf(
"%s: service command %q references $%s unquoted; "+
"wrap it in double quotes so it survives project paths with spaces",
file, command, name,
)
}
}
}
}
})
}
}

// processComposeFiles returns the embedded path of every builtin plugin's
// process-compose.yaml.
func processComposeFiles(t *testing.T) []string {
t.Helper()
files, err := fs.Glob(builtIn, "*/process-compose.yaml")
if err != nil {
t.Fatalf("globbing process-compose files: %v", err)
}
if len(files) == 0 {
t.Fatal("no process-compose.yaml files found")
}
return files
}

// serviceCommands parses a process-compose.yaml and returns every "command"
// string it defines (start, shutdown, and readiness-probe commands alike).
func serviceCommands(t *testing.T, file string) []string {
t.Helper()
contents, err := fs.ReadFile(builtIn, file)
if err != nil {
t.Fatalf("reading %s: %v", file, err)
}

var doc any
if err := yaml.Unmarshal(contents, &doc); err != nil {
t.Fatalf("parsing %s: %v", file, err)
}

var commands []string
collectCommands(doc, &commands)
return commands
}

// collectCommands walks a decoded YAML document and appends the string value
// of every mapping entry whose key is "command".
func collectCommands(node any, out *[]string) {
switch val := node.(type) {
case map[string]any:
for key, child := range val {
if key == "command" {
if s, ok := child.(string); ok {
*out = append(*out, s)
continue
}
}
collectCommands(child, out)
}
case map[any]any:
for key, child := range val {
if key == "command" {
if s, ok := child.(string); ok {
*out = append(*out, s)
continue
}
}
collectCommands(child, out)
}
case []any:
for _, child := range val {
collectCommands(child, out)
}
}
}

// envVarIndices returns the index of the leading '$' for every reference to the
// named environment variable in line, matching both the "$NAME" and "${NAME}"
// forms. The "$NAME" form only matches when NAME is not immediately followed by
// another identifier character, so "$FOO" does not match a search for "$F".
func envVarIndices(line, name string) []int {
var indices []int
for _, pattern := range []string{"${" + name + "}", "$" + name} {
for start := 0; ; {
i := strings.Index(line[start:], pattern)
if i < 0 {
break
}
at := start + i
end := at + len(pattern)
// For the bare "$NAME" form, reject a longer identifier match.
if !strings.HasSuffix(pattern, "}") && end < len(line) && isIdentByte(line[end]) {
start = at + 1
continue
}
indices = append(indices, at)
start = end
}
}
return indices
}

func isIdentByte(b byte) bool {
return b == '_' ||
(b >= '0' && b <= '9') ||
(b >= 'a' && b <= 'z') ||
(b >= 'A' && b <= 'Z')
}
2 changes: 1 addition & 1 deletion plugins/valkey.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "valkey",
"version": "0.0.1",
"version": "0.0.2",
"description": "Running `devbox services start valkey` will start valkey as a daemon in the background. \n\nYou can manually start Valkey in the foreground by running `valkey-server $VALKEY_CONF --port $VALKEY_PORT`. \n\nLogs, pidfile, and data dumps are stored in `.devbox/virtenv/valkey`. You can change this by modifying the `dir` directive in `devbox.d/valkey/valkey.conf`",
"env": {
"VALKEY_PORT": "6379",
Expand Down
2 changes: 1 addition & 1 deletion plugins/valkey/process-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "0.5"

processes:
valkey:
command: "valkey-server $VALKEY_CONF --port $VALKEY_PORT"
command: "valkey-server \"$VALKEY_CONF\" --port $VALKEY_PORT"
availability:
restart: on_failure
max_restarts: 5
Loading