Support for compose files with different names (as per spec)#33
Support for compose files with different names (as per spec)#33mstrhakr wants to merge 3 commits intomstrhakr:mainfrom
Conversation
Implemented feature to support different file names (per spec https://github.com/compose-spec/compose-spec/blob/main/spec.md#compose-file) for the compose files
Saving files with other names for compose file as per spec now works
|
Hey @mstrhakr just noticed you created a PR with my changes i tried to implement into upstream. If you need anything from my side or if i can help in any way, please let me know :) |
There was a problem hiding this comment.
Pull request overview
Adds Compose-spec compliant discovery for alternate compose filenames (compose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml) across the plugin’s shell scripts and PHP/UI flows, addressing issue #31.
Changes:
- Introduces PHP helpers to locate the primary compose file and derive override filenames.
- Updates PHP endpoints/UI to read/write the discovered compose file rather than assuming
docker-compose.yml. - Updates boot/shutdown event scripts and wrapper logic to recognize the additional compose filenames.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
source/compose.manager/scripts/compose.sh |
Expands compose-file discovery for -d project dirs and refactors image cleanup fallback. |
source/compose.manager/php/util.php |
Adds findComposeFile() + getComposeFileBaseName() helpers used across PHP. |
source/compose.manager/php/exec.php |
Uses helper-based compose/override selection for get/save operations. |
source/compose.manager/php/compose_util.php |
Uses helper-based compose selection and expands override inclusion logic. |
source/compose.manager/php/compose_manager_main.php |
Updates editor behavior to track the actual compose filename and accept .yaml. |
source/compose.manager/event/started |
Extends autostart handling to non-docker-compose.yml filenames. |
source/compose.manager/event/stopping_docker |
Extends stop handling to non-docker-compose.yml filenames. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $composeCommand[] = $composeOverride; | ||
| if ( $debug ) { | ||
| logger("Using project override file: $overrideFile"); | ||
| } | ||
| } | ||
| } else { | ||
| // Check for both yml and yaml override files | ||
| if (is_file("$basePath/docker-compose.override.yml")) { | ||
| $composeOverride = "-f$basePath/docker-compose.override.yml"; | ||
| $composeCommand[] = $composeOverride; | ||
| if ( $debug ) { | ||
| logger("Using project override file: $basePath/docker-compose.override.yml"); | ||
| } | ||
| } else if (is_file("$basePath/docker-compose.override.yaml")) { | ||
| $composeOverride = "-f$basePath/docker-compose.override.yaml"; | ||
| $composeCommand[] = $composeOverride; | ||
| if ( $debug ) { | ||
| logger("Using project override file: $basePath/docker-compose.override.yaml"); |
There was a problem hiding this comment.
The override resolution can add the same override file twice. For example, if the base compose file is docker-compose.yml and $compose_root/<project>/docker-compose.override.yml exists, it’s added once in the “plugin override” block and again in the “project override” block ($overrideFile). Consider de-duping -f entries and/or clearly separating “plugin labels override” from “user override” to avoid redundant arguments and ambiguity about precedence.
| $composeCommand[] = $composeOverride; | |
| if ( $debug ) { | |
| logger("Using project override file: $overrideFile"); | |
| } | |
| } | |
| } else { | |
| // Check for both yml and yaml override files | |
| if (is_file("$basePath/docker-compose.override.yml")) { | |
| $composeOverride = "-f$basePath/docker-compose.override.yml"; | |
| $composeCommand[] = $composeOverride; | |
| if ( $debug ) { | |
| logger("Using project override file: $basePath/docker-compose.override.yml"); | |
| } | |
| } else if (is_file("$basePath/docker-compose.override.yaml")) { | |
| $composeOverride = "-f$basePath/docker-compose.override.yaml"; | |
| $composeCommand[] = $composeOverride; | |
| if ( $debug ) { | |
| logger("Using project override file: $basePath/docker-compose.override.yaml"); | |
| if (!in_array($composeOverride, $composeCommand, true)) { | |
| $composeCommand[] = $composeOverride; | |
| if ( $debug ) { | |
| logger("Using project override file: $overrideFile"); | |
| } | |
| } else if ( $debug ) { | |
| logger("Skipping duplicate project override file: $overrideFile"); | |
| } | |
| } | |
| } else { | |
| // Check for both yml and yaml override files | |
| if (is_file("$basePath/docker-compose.override.yml")) { | |
| $composeOverride = "-f$basePath/docker-compose.override.yml"; | |
| if (!in_array($composeOverride, $composeCommand, true)) { | |
| $composeCommand[] = $composeOverride; | |
| if ( $debug ) { | |
| logger("Using project override file: $basePath/docker-compose.override.yml"); | |
| } | |
| } else if ( $debug ) { | |
| logger("Skipping duplicate project override file: $basePath/docker-compose.override.yml"); | |
| } | |
| } else if (is_file("$basePath/docker-compose.override.yaml")) { | |
| $composeOverride = "-f$basePath/docker-compose.override.yaml"; | |
| if (!in_array($composeOverride, $composeCommand, true)) { | |
| $composeCommand[] = $composeOverride; | |
| if ( $debug ) { | |
| logger("Using project override file: $basePath/docker-compose.override.yaml"); | |
| } | |
| } else if ( $debug ) { | |
| logger("Skipping duplicate project override file: $basePath/docker-compose.override.yaml"); |
| # Find the compose file | ||
| compose_file="" | ||
| if [ -f "$dir/compose.yaml" ]; then | ||
| compose_file="$dir/compose.yaml" | ||
| override_file="$dir/compose.override.yml" | ||
| elif [ -f "$dir/compose.yml" ]; then | ||
| compose_file="$dir/compose.yml" | ||
| override_file="$dir/compose.override.yml" | ||
| elif [ -f "$dir/docker-compose.yaml" ]; then | ||
| compose_file="$dir/docker-compose.yaml" | ||
| override_file="$dir/docker-compose.override.yml" | ||
| elif [ -f "$dir/docker-compose.yml" ]; then | ||
| compose_file="$dir/docker-compose.yml" | ||
| override_file="$dir/docker-compose.override.yml" | ||
| fi | ||
|
|
||
| override="" | ||
| if [ -f "$dir/docker-compose.override.yml" ]; then | ||
| override="$dir/docker-compose.override.yml" | ||
| if [ -n "$compose_file" ] && [ -f "$override_file" ]; then | ||
| override="$override_file" | ||
| override="-f ${override@Q}" | ||
| fi |
There was a problem hiding this comment.
Same issue as event/started: for .yaml compose files this hard-codes a .yml override name, and for indirect stacks compose_file is empty so overrides in the project directory are never applied. This can prevent label overrides from being used and change stopping behavior compared to previous versions. Update override detection to support .override.yaml and to include the project’s override even when indirect is used.
| # Function to find compose files with both .yml and .yaml extensions | ||
| find_compose_files() { | ||
| local dir="$1" | ||
| # First find all .yml files | ||
| find "$dir" -maxdepth 1 -type f -name '*compose*.yml' -print | ||
| # Then find all .yaml files | ||
| find "$dir" -maxdepth 1 -type f -name '*compose*.yaml' -print |
There was a problem hiding this comment.
find_compose_files currently collects all *compose*.yml and *compose*.yaml files and the caller adds every match as a -f argument. This means if both compose.yaml and compose.yml exist they’ll both be merged instead of preferring the canonical compose.yaml per spec, and override files may be included in an arbitrary order. Consider selecting a single base file in the documented priority order (compose.yaml → compose.yml → docker-compose.yaml → docker-compose.yml) and then (optionally) appending only its matching override file after it.
| # Function to find compose files with both .yml and .yaml extensions | |
| find_compose_files() { | |
| local dir="$1" | |
| # First find all .yml files | |
| find "$dir" -maxdepth 1 -type f -name '*compose*.yml' -print | |
| # Then find all .yaml files | |
| find "$dir" -maxdepth 1 -type f -name '*compose*.yaml' -print | |
| # Function to find compose files following Docker Compose priority: | |
| # compose.yaml → compose.yml → docker-compose.yaml → docker-compose.yml | |
| # Optionally appends a matching override file (e.g. compose.override.yaml). | |
| find_compose_files() { | |
| local dir="$1" | |
| # Define base files in priority order | |
| local base_files=("compose.yaml" "compose.yml" "docker-compose.yaml" "docker-compose.yml") | |
| local base_file="" | |
| for candidate in "${base_files[@]}"; do | |
| if [ -f "$dir/$candidate" ]; then | |
| base_file="$dir/$candidate" | |
| break | |
| fi | |
| done | |
| # If no base file was found, return without output | |
| if [ -z "$base_file" ]; then | |
| return 0 | |
| fi | |
| # Echo the selected base file | |
| echo "$base_file" | |
| # Look for a matching override file with the same extension: | |
| # e.g. compose.yaml -> compose.override.yaml | |
| local ext="${base_file##*.}" | |
| local stem="${base_file%.*}" | |
| local override_file="${stem}.override.${ext}" | |
| if [ -f "$override_file" ]; then | |
| echo "$override_file" | |
| fi |
| ]; | ||
|
|
||
| foreach ($possibleFiles as $file) { | ||
| if (file_exists($file)) { |
There was a problem hiding this comment.
findComposeFile() uses file_exists(), which will also return true for directories. Since callers assume the result is readable YAML (e.g., pass it to file_get_contents() / pathinfo()), this can lead to warnings or incorrect behavior if a directory happens to match one of these names. Prefer is_file() (and optionally is_link() if symlinks are expected).
| if (file_exists($file)) { | |
| if (is_file($file)) { |
| # Find the compose file | ||
| compose_file="" | ||
| if [ -f "$dir/compose.yaml" ]; then | ||
| compose_file="$dir/compose.yaml" | ||
| override_file="$dir/compose.override.yml" | ||
| elif [ -f "$dir/compose.yml" ]; then | ||
| compose_file="$dir/compose.yml" | ||
| override_file="$dir/compose.override.yml" | ||
| elif [ -f "$dir/docker-compose.yaml" ]; then | ||
| compose_file="$dir/docker-compose.yaml" | ||
| override_file="$dir/docker-compose.override.yml" | ||
| elif [ -f "$dir/docker-compose.yml" ]; then | ||
| compose_file="$dir/docker-compose.yml" | ||
| override_file="$dir/docker-compose.override.yml" | ||
| fi | ||
|
|
||
| override="" | ||
| if [ -f "$dir/docker-compose.override.yml" ]; then | ||
| override="$dir/docker-compose.override.yml" | ||
| if [ -n "$compose_file" ] && [ -f "$override_file" ]; then | ||
| override="$override_file" | ||
| override="-f ${override@Q}" | ||
| fi |
There was a problem hiding this comment.
Override selection is incorrect for .yaml compose files and indirect stacks. For compose.yaml / docker-compose.yaml this sets override_file to a .yml name, so .override.yaml files won’t be picked up. Also, for indirect stacks (only $dir/indirect present), compose_file stays empty and the override is skipped entirely due to -n "$compose_file", which is a regression from the previous docker-compose.override.yml check. Consider: (1) deriving the override filename from the chosen compose file’s extension/base name (or checking both .yml and .yaml), and (2) allowing the plugin override in the project dir even when the stack is indirect.
| delete="-f" | ||
| files_arr=( $files ) | ||
| files_arr=( $files ) | ||
| files_arr=( ${files_arr[@]/$delete} ) | ||
| if (( ${#files_arr[@]} )); then | ||
| services=( $(cat ${files_arr[*]//\'/} | sed -n 's/image:\(.*\)/\1/p') ) | ||
|
|
||
| for image in "${services[@]}"; do | ||
| images+=( $(docker images -q --no-trunc ${image}) ) | ||
| # Process each file individually to handle paths with spaces or special characters | ||
| for file_path in "${files_arr[@]}"; do | ||
| # Remove surrounding quotes if present | ||
| file_path="${file_path//\'}" | ||
| file_path="${file_path//\"}" | ||
|
|
There was a problem hiding this comment.
The update fallback tries to “handle paths with spaces” but files_arr=( $files ) still performs word-splitting on the $files string; quotes produced by ${file@Q} are just characters at that point and won’t prevent splitting. This can cause image cleanup to miss compose files or read incorrect paths. Consider storing compose -f arguments in an array from the start (instead of a concatenated string) so you can iterate file paths without reparsing.
| // If no compose file exists, use the default path | ||
| if ($composeFile === null) { | ||
| // Try both yaml and yml extensions | ||
| if (is_file("$basePath/docker-compose.yaml")) { | ||
| $fileName = "docker-compose.yaml"; | ||
| } else { | ||
| $fileName = "docker-compose.yml"; | ||
| } |
There was a problem hiding this comment.
In getYml, the branch that checks is_file("$basePath/docker-compose.yaml") is unreachable: if that file existed, findComposeFile($basePath) would have returned it. This extra logic is confusing and can be removed/simplified (e.g., pick a single default when no compose file exists).
| // If no compose file exists, use the default path | |
| if ($composeFile === null) { | |
| // Try both yaml and yml extensions | |
| if (is_file("$basePath/docker-compose.yaml")) { | |
| $fileName = "docker-compose.yaml"; | |
| } else { | |
| $fileName = "docker-compose.yml"; | |
| } | |
| // If no compose file exists, use a single default path | |
| if ($composeFile === null) { | |
| $fileName = "docker-compose.yml"; |
| // If no compose file exists, use the default path | ||
| if ($composeFile === null) { | ||
| // Try both yaml and yml extensions | ||
| if (is_file("$basePath/docker-compose.yaml")) { | ||
| $fileName = "docker-compose.yaml"; | ||
| } else { | ||
| $fileName = "docker-compose.yml"; | ||
| } |
There was a problem hiding this comment.
Same as getYml: in saveYml, checking for docker-compose.yaml inside the ($composeFile === null) branch is dead code because findComposeFile($basePath) would have found it. Simplifying this will make the save path behavior clearer.
| // If no compose file exists, use the default path | |
| if ($composeFile === null) { | |
| // Try both yaml and yml extensions | |
| if (is_file("$basePath/docker-compose.yaml")) { | |
| $fileName = "docker-compose.yaml"; | |
| } else { | |
| $fileName = "docker-compose.yml"; | |
| } | |
| // If no compose file exists, use a default path | |
| if ($composeFile === null) { | |
| $fileName = "docker-compose.yml"; |
| # Extract image names from the file | ||
| file_services=( $(cat "$file_path" | sed -n 's/image:\(.*\)/\1/p') ) | ||
| for image in "${file_services[@]}"; do | ||
| images+=( $(docker images -q --no-trunc ${image}) ) |
There was a problem hiding this comment.
The loop that builds images uses docker images -q --no-trunc ${image} with image coming directly from untrusted compose files (image: lines) without quoting, inside a command substitution. An attacker who controls a compose file can set an image value containing shell metacharacters (e.g. busybox; rm -rf /) so that when this line executes, the shell runs additional arbitrary commands as root before or after the docker images invocation. To fix this, ensure the image name is never interpreted by the shell (e.g., avoid unquoted expansion and avoid constructing the command in a way that allows image contents to break out into separate shell commands).
| images+=( $(docker images -q --no-trunc ${image}) ) | |
| images+=( $(docker images -q --no-trunc -- "$image") ) |
| // Extract the actual filename from the response | ||
| var filename = response.fileName.split('/').pop(); | ||
| $('#editorFileName').data("stackfilename", filename) | ||
| $('#editorFileName').html(response.fileName) |
There was a problem hiding this comment.
response.fileName is inserted into the DOM via $('#editorFileName').html(response.fileName) without any HTML-encoding, but its value ultimately depends on user-controlled paths (e.g., an indirect stack path set via stackPath). An attacker can supply a path containing HTML/JavaScript (such as <script> tags) so that when the compose editor is opened, arbitrary script executes in the admin’s browser. To mitigate this, treat response.fileName as plain text (e.g., use a text-only setter or explicit HTML-escaping) rather than inserting it as raw HTML.
|
Far too many issues trying to merge from the old PR, this did provide some reference while I implemented this feature. Closing this PR but this feature is being implemented manually. |
PR from HERE
Requested HERE
Fixes #31
This would be correct behavior according to the docs: