Skip to content

Comments

Support for compose files with different names (as per spec)#33

Closed
mstrhakr wants to merge 3 commits intomstrhakr:mainfrom
Joly0:main
Closed

Support for compose files with different names (as per spec)#33
mstrhakr wants to merge 3 commits intomstrhakr:mainfrom
Joly0:main

Conversation

@mstrhakr
Copy link
Owner

PR from HERE

Requested HERE

Fixes #31

This would be correct behavior according to the docs:

The default path for a Compose file is compose.yaml (preferred) or compose.yml that is placed in the working directory. Compose also supports docker-compose.yaml and docker-compose.yml for backwards compatibility of earlier versions. If both files exist, Compose prefers the canonical compose.yaml.

Joly0 added 3 commits May 14, 2025 14:01
Saving files with other names for compose file as per spec now works
@mstrhakr mstrhakr self-assigned this Feb 11, 2026
@Joly0
Copy link

Joly0 commented Feb 13, 2026

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 :)

@mstrhakr mstrhakr marked this pull request as ready for review February 17, 2026 16:36
Copilot AI review requested due to automatic review settings February 17, 2026 16:36
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +109 to +126
$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");
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
$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");

Copilot uses AI. Check for mistakes.
Comment on lines +15 to 35
# 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
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +10
# 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
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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

Copilot uses AI. Check for mistakes.
];

foreach ($possibleFiles as $file) {
if (file_exists($file)) {
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
if (file_exists($file)) {
if (is_file($file)) {

Copilot uses AI. Check for mistakes.
Comment on lines +37 to 57
# 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
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 112 to +121
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//\"}"

Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +107
// 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";
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
// 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";

Copilot uses AI. Check for mistakes.
Comment on lines +169 to +176
// 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";
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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";

Copilot uses AI. Check for mistakes.
# 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}) )
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
images+=( $(docker images -q --no-trunc ${image}) )
images+=( $(docker images -q --no-trunc -- "$image") )

Copilot uses AI. Check for mistakes.
// Extract the actual filename from the response
var filename = response.fileName.split('/').pop();
$('#editorFileName').data("stackfilename", filename)
$('#editorFileName').html(response.fileName)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@mstrhakr
Copy link
Owner Author

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.

@mstrhakr mstrhakr closed this Feb 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support of compose files in accordance with specifications

2 participants