-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Description
Description
Hello, I noticed an edge case bug in the code that validates paths in compose watch.
Indeed, the code checks whether any bind mount volume is a prefix to the watched path, but doing so naively causes it to falsy assumes that a sibling directory with a similar name is a prefix as well.
For example, this is one bind mount volume falsely considered as a prefix to the watched path:
- Bind mount volume:
/home/user/dir - Watched path:
/home/user/dir2
This is checked in pkg/compose/watch.go here:
if isSync(trigger) && checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {With the underlying function checkIfPathAlreadyBindMounted checking this:
if volume.Bind != nil && strings.HasPrefix(watchPath, volume.Source) {This would compute strings.HasPrefix("/home/user/dir2", "/home/user/dir"), which is true but not the expected behavior.
Steps To Reproduce
Making it a bit fun...
Demonstrating a false positive conflict between:
- A bind mount volume:
./data - A watched path:
./data-init
Generate files to reproduce my example environment:
# Create a new directory in which to run the test
mkdir -p ./test-issue
cd ./test-issue
# Create the directories
mkdir -p ./app
mkdir -p ./data
mkdir -p ./data-init
# Create the Compose file
cat <<'EOF' > ./compose.yml
services:
data-init:
# Build
build:
context: .
dockerfile_inline: |
FROM alpine:3.21
WORKDIR /app
COPY --link ./data-init .
CMD [ "sh", "./init.sh" ]
# Deploy
environment:
LOOP_INIT: "${LOOP_INIT:-false}"
volumes:
- ./data:/data
develop:
watch:
# Docker image
- action: sync
path: ./data-init
target: /app
app:
# Build
build:
context: .
dockerfile_inline: |
FROM alpine:3.21
WORKDIR /app
COPY --link ./app .
CMD [ "sh", "./hello.sh" ]
# Deploy
depends_on:
data-init:
condition: service_completed_successfully
environment:
LOOP_APP: "${LOOP_APP:-false}"
volumes:
- ./data:/data
develop:
watch:
# Docker image
- action: sync
path: ./app
target: /app
EOF
# Create ./app/hello.sh start script for the app service
cat <<'EOF' > ./app/hello.sh
#!/bin/sh
echo "Hello, there!"
FIRST_DATA_FILE=$(ls /data | head -n 1)
if [ -z "$FIRST_DATA_FILE" ]; then
echo "I found no data in /data."
echo "I will cry now."
exit 1
fi
echo "I found a file in /data: $FIRST_DATA_FILE"
echo "It contains..."
FIRST_DATA_FILE_CONTENTS=$(cat /data/$FIRST_DATA_FILE)
echo "'$FIRST_DATA_FILE_CONTENTS'"
echo "Great."
if [ "$LOOP_APP" = "true" ]; then
echo "I will loop forever now."
while true; do
sleep 1
done
fi
EOF
# Create ./data-init/init.sh start script for the data-init service
cat <<'EOF' > ./data-init/init.sh
#!/bin/sh
echo "Hohoho!"
rm -rf /data/*
echo "I removed all the data in /data."
RANDOM_FILE_NAME=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 8)
echo "Some data in a random file!" > /data/$RANDOM_FILE_NAME.txt
echo "I dropped some data in a random file: /data/$RANDOM_FILE_NAME.txt"
echo "Goodbye!"
if [ "$LOOP_INIT" = "true" ]; then
echo "I will loop forever now."
while true; do
sleep 1
done
fi
EOFThen, let's check it works:
docker compose upWe get the following output:
[+] Running 5/5
✔ app Built 0.0s
✔ data-init Built 0.0s
✔ Network test-issue_default Created 0.0s
✔ Container test-issue-data-init-1 Created 0.0s
✔ Container test-issue-app-1 Created 0.0s
Attaching to app-1, data-init-1
data-init-1 | Hohoho!
data-init-1 | I removed all the data in /data.
data-init-1 | I dropped some data in a random file: /data/AfYFPeAm.txt
data-init-1 | Goodbye!
data-init-1 exited with code 0
app-1 | Hello, there!
app-1 | I found a file in /data: AfYFPeAm.txt
app-1 | It contains...
app-1 | 'Some data in a random file!'
app-1 | Great.
app-1 exited with code 0
Now, to see compose watch alert us about the bind mount volume, we can run:
docker compose up --watchWe get the following output:
[+] Running 2/2
✔ Container test-issue-data-init-1 Created 0.0s
✔ Container test-issue-app-1 Created 0.0s
WARN[0000] path '/[...]/test-issue/data-init' also declared by a bind mount volume, this path won't be monitored!
⦿ Watch enabled
data-init-1 | Hohoho!
data-init-1 | I removed all the data in /data.
data-init-1 | I dropped some data in a random file: /data/2lJfV7dw.txt
data-init-1 | Goodbye!
data-init-1 exited with code 0
app-1 | Hello, there!
app-1 | I found a file in /data: JDGsDWEk.txt
app-1 | It contains...
app-1 | 'Some data in a random file!'
app-1 | Great.
app-1 exited with code 0
To test live that the watch path is ignored, I've added env vars to freeze a service in a loop.
Freezing the app service:
LOOP_APP=true docker compose up --watchWe get the following output:
[+] Running 2/2
✔ Container test-issue-data-init-1 Recreated 0.1s
✔ Container test-issue-app-1 Recreated 0.1s
WARN[0000] path '/[...]/test-issue/data-init' also declared by a bind mount volume, this path won't be monitored!
⦿ Watch enabled
data-init-1 | Hohoho!
data-init-1 | I removed all the data in /data.
data-init-1 | I dropped some data in a random file: /data/2lJfV7dw.txt
data-init-1 | Goodbye!
data-init-1 exited with code 0
app-1 | Hello, there!
app-1 | I found a file in /data: 2lJfV7dw.txt
app-1 | It contains...
app-1 | 'Some data in a random file!'
app-1 | Great.
app-1 | I will loop forever now.
Now, if we modify the scripts...
- Modifying
./app/hello.shtriggers compose watch:⦿ Syncing service "app" after 1 changes were detected - Modifying
./data-init/init.shdoes nothing!
Finally, freezing the data-init service:
LOOP_INIT=true docker compose up --watchWe get the following output:
[+] Running 2/2
✔ Container test-issue-app-1 Recreated 0.1s
✔ Container test-issue-data-init-1 Recreated 0.1s
WARN[0000] path '/[...]/test-issue/data-init' also declared by a bind mount volume, this path won't be monitored!
⦿ Watch enabled
Attaching to app-1, data-init-1
data-init-1 | Hohoho!
data-init-1 | I removed all the data in /data.
data-init-1 | I dropped some data in a random file: /data/aX2QXT8x.txt
data-init-1 | Goodbye!
data-init-1 | I will loop forever now.
Then, if we modify the scripts...
- Modifying
./app/hello.shdoes still trigger compose watch:⦿ Syncing service "app" after 1 changes were detected - Modifying
./data-init/init.shdoes nothing again!
Compose Version
Docker Compose version v2.33.1
Docker Environment
(`docker -v` ought to be enough...)
Docker version 28.0.1, build 068a01e
Anything else?
I'll create a PR to propose a fix for this issue, as I already found the problematic code and this should be a simple fix.