Skip to content

Commit e5dcf8f

Browse files
committed
tests: Implemented volume state safety tests
Implemented tests for behavior when mount and unmount operations partially fail (in those cases, docker-on-top must detect that and either refuse to mount the volume, refuse to mount the volume on the next attempt, or recover on the next attempt, but not bring the volume to an unsafe state). + extracted a common part from basic_usage.bats to common.sh
1 parent 05618a4 commit e5dcf8f

File tree

3 files changed

+225
-17
lines changed

3 files changed

+225
-17
lines changed

tests/basic_usage.bats

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#!/usr/bin/env bats
22

3+
# For `CONTAINER_CMD_*`
4+
load common.sh
5+
36
basic_test() {
47
# Setup
58
BASE="$(mktemp --directory)"
@@ -18,23 +21,11 @@ basic_test() {
1821
echo 456 > "$BASE"/b
1922

2023
docker run --rm -v "$NAME":/dot alpine:latest \
21-
sh -e -c '
22-
# Initial data is visible
23-
[ "$(cat /dot/a)" = 123 ]
24-
[ "$(cat /dot/b)" = 456 ]
25-
26-
# My file removal is visible to me
27-
rm /dot/a
28-
[ ! -e /dot/a ]
29-
30-
# My changes are visible to me
31-
echo 789 > /dot/b
32-
[ "$(cat /dot/b)" = 789 ]
33-
34-
# My new files are visible to me
35-
echo etc > /dot/c
36-
[ "$(cat /dot/c)" = etc ]
37-
'
24+
sh -e -c "
25+
$CONTAINER_CMD_CHECK_INITIAL_DATA
26+
27+
$CONTAINER_CMD_MAKE_AND_CHECK_CHANGES
28+
"
3829

3930
# Changes are not visible from the host
4031
[ "$(cat "$BASE"/a)" = 123 ]

tests/common.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/false
2+
3+
CONTAINER_CMD_CHECK_INITIAL_DATA='
4+
# Check that the initial data is as expected
5+
[ "$(cat /dot/a)" = 123 ]
6+
[ "$(cat /dot/b)" = 456 ]
7+
'
8+
9+
CONTAINER_CMD_MAKE_AND_CHECK_CHANGES='
10+
# My file removal is visible to me
11+
rm -f /dot/a
12+
[ ! -e /dot/a ]
13+
14+
# My changes are visible to me
15+
echo 789 > /dot/b
16+
[ "$(cat /dot/b)" = 789 ]
17+
18+
# My new files are visible to me
19+
echo etc > /dot/c
20+
[ "$(cat /dot/c)" = etc ]
21+
'
22+
23+
CONTAINER_CMD_CHECK_ALREADY_MODIFIED_DATA='
24+
[ ! -e /dot/a ]
25+
[ "$(cat /dot/b)" = 789 ]
26+
[ "$(cat /dot/c)" = etc ]
27+
'
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#!/usr/bin/env bats
2+
3+
# TODO: move the following explanation from here to the source code?
4+
5+
# Under normal operation, docker-on-top keeps all of its volumes in a
6+
# _valid_ state. When interfered with (e.g., when the user messes with
7+
# our internal state or in case we were terminated abruptly or whatever
8+
# other reason causes our crucial syscalls to fail),
9+
# it may not be possible to maintain a _valid_ state, but we still want to
10+
# leave a volume in a state that is _safe_, that is, on the next interaction
11+
# with the volume, docker-on-top can tell that something went wrong last time.
12+
13+
# For example, if a volume is unmounted improperly, such that `activemountsdir`
14+
# was cleaned properly but we failed to unmount `mountpointdir`, that's unfortunate
15+
# (as we leave a mount hanging) but tolerable (as the next attempt to mount it will
16+
# succeed as if the volume was unmounted cleanly).
17+
#
18+
# On the contrary, if a volume is unmounted improperly, such that `mountpointdir`
19+
# was unmounted but `activemountsdir` remains non-empty, that's a horror. On the
20+
# next attempt to mount that volume, docker-on-top will see that it is already mounted
21+
# and will try to use `mountpoindir` outright but, as it is not actually mounted, that
22+
# will lead to a completely broken behavior.
23+
24+
# The following tests try to simulate that either mount/unmount system calls, or
25+
# activemounts file creation/removal system calls fail, and tests that docker-on-top
26+
# is fine.
27+
28+
29+
# For `CONTAINER_CMD_*`
30+
load common.sh
31+
32+
# Ask for password early
33+
sudo -v
34+
35+
# Test template
36+
37+
# $1 is empty if volatile, non-empty if non-volatile;
38+
# $2 is the command (messing with the state) to perform before mounting;
39+
# $3 is the command (messing with the state) to perform before unmounting;
40+
# $4 shall undo the effect of $2;
41+
# $5 shall undo the effect of $3.
42+
template() {
43+
# Setup
44+
BASE="$(mktemp --directory)"
45+
NAME="$(basename "$BASE")"
46+
if [ -z "$1" ]; then
47+
VOLATILE=false
48+
else
49+
VOLATILE=true
50+
fi
51+
docker volume create --driver docker-on-top "$NAME" -o base="$BASE" -o volatile="$VOLATILE"
52+
53+
echo 123 > "$BASE"/a
54+
echo 456 > "$BASE"/b
55+
56+
# Run the breaking action before mount
57+
$2 "$BASE" "$NAME"
58+
59+
# Start the container (might fail)
60+
if ! CONTAINER_ID=$(
61+
docker run -d -v "$NAME":/dot alpine:latest \
62+
sh -e -c "
63+
sleep 1 # Give as a moment to break everything
64+
65+
$CONTAINER_CMD_CHECK_INITIAL_DATA
66+
67+
$CONTAINER_CMD_MAKE_AND_CHECK_CHANGES
68+
"
69+
); then
70+
71+
# If it has failed, we are most lucky: docker-on-top has detected the problem
72+
# so early it did not break anything in the first place
73+
true; return
74+
fi
75+
76+
# Undo the breaking action
77+
$4 "$BASE" "$NAME"
78+
79+
# Run the breaking action before unmount
80+
$3 "$BASE" "$NAME"
81+
82+
if [ 0 -ne "$(docker wait "$CONTAINER_ID")" ]; then
83+
# Container has failed
84+
docker rm "$CONTAINER_ID"
85+
false
86+
return
87+
fi
88+
docker rm "$CONTAINER_ID"
89+
90+
# Undo the breaking action
91+
$5 "$BASE" "$NAME"
92+
93+
# Now that we are fixed, we expect that either the container won't stat at all:
94+
if ! docker run --rm -v "$NAME":/dot alpine:latest true; then
95+
return # The test is successful: docker-on-top detected the error and the mount is refused
96+
fi
97+
98+
# Or a container should start and run as expected:
99+
if $VOLATILE; then
100+
docker run --rm -v "$NAME":/dot alpine:latest \
101+
sh -e -c "
102+
$CONTAINER_CMD_CHECK_INITIAL_DATA
103+
104+
$CONTAINER_CMD_MAKE_AND_CHECK_CHANGES
105+
"
106+
else
107+
docker run --rm -v "$NAME":/dot alpine:latest \
108+
sh -e -c "
109+
$CONTAINER_CMD_CHECK_ALREADY_MODIFIED_DATA
110+
111+
$CONTAINER_CMD_MAKE_AND_CHECK_CHANGES
112+
"
113+
fi
114+
115+
# Cleanup (also help docker-on-top with what we may have broken)
116+
sudo umount /var/lib/docker-on-top/"$NAME"/mountpoint || :
117+
sudo rmdir /var/lib/docker-on-top/"$NAME"/mountpoint || :
118+
sudo rm /var/lib/docker-on-top/"$NAME"/activemounts/* || :
119+
rm -rf "$BASE"
120+
docker volume rm "$NAME"
121+
}
122+
123+
124+
# I use "activate"/"deactivate" as shorts for "create/remove a file at `activemountsdir`"
125+
126+
# Break/unbreak functions receive $BASE as $1 and $NAME as $2
127+
128+
break_deactivate() {
129+
# Immutable file can not be removed
130+
sudo chattr +i /var/lib/docker-on-top/"$2"/activemounts/*
131+
}
132+
133+
unbreak_deactivate() {
134+
sudo chattr -i /var/lib/docker-on-top/"$2"/activemounts/*
135+
}
136+
137+
138+
@test "Mount=fine, activate=fine, unmount=fine, deactivate=broken" {
139+
template "" : : break_deactivate unbreak_deactivate
140+
141+
template "volatile" : : break_deactivate unbreak_deactivate
142+
}
143+
144+
break_unmount() {
145+
# The mountpoint will be kept busy with the following process that has a file on it open
146+
sleep 1.5 < /var/lib/docker-on-top/"$2"/mountpoint/b
147+
}
148+
149+
unbreak_unmount() {
150+
# Just wait for the previous process to finish
151+
sleep 1.5
152+
}
153+
154+
155+
@test "Mount=fine, activate=fine, unmount=broken, deactivate=fine" {
156+
skip "This is not working yet, needs to be fixed"
157+
158+
template "" : : break_unmount unbreak_unmount
159+
160+
template "volatile" : : break_unmount unbreak_unmount
161+
}
162+
163+
break_activate() {
164+
sudo chattr +i /var/lib/docker-on-top/"$2"/activemounts
165+
}
166+
167+
unbreak_activate() {
168+
sudo chattr -i /var/lib/docker-on-top/"$2"/activemounts
169+
}
170+
171+
@test "Mount=fine, activate=broken, unmount=fine, deactivate=fine" {
172+
template "" break_activate unbreak_activate : :
173+
174+
template "volatile" break_activate unbreak_activate : :
175+
}
176+
177+
break_mount() {
178+
# If the base directory does not exist, the overlayfs mount shall fail
179+
rm -r "$1"
180+
}
181+
182+
unbreak_mount() {
183+
mkdir "$1"
184+
}
185+
186+
@test "Mount=broken, activate=fine, unmount=fine, deactivate=fine" {
187+
template "" break_mount unbreak_mount : :
188+
189+
template "volatile" break_mount unbreak_mount : :
190+
}

0 commit comments

Comments
 (0)