Skip to content

Commit 9a1ead8

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 75bf60d commit 9a1ead8

File tree

3 files changed

+232
-17
lines changed

3 files changed

+232
-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: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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+
template_cleanup() {
38+
$3 "$BASE" "$NAME"
39+
$5 "$BASE" "$NAME"
40+
sudo umount /var/lib/docker-on-top/"$NAME"/mountpoint || :
41+
sudo rmdir /var/lib/docker-on-top/"$NAME"/mountpoint || :
42+
sudo rm /var/lib/docker-on-top/"$NAME"/activemounts/* || :
43+
docker rm "$CONTAINER_ID"
44+
docker volume rm "$NAME"
45+
rm -rf "$BASE"
46+
}
47+
48+
# $1 is empty if volatile, non-empty if non-volatile;
49+
# $2 is the command (messing with the state) to perform before mounting;
50+
# $3 shall undo the effect of $2;
51+
# $4 is the command (messing with the state) to perform before unmounting;
52+
# $5 shall undo the effect of $4.
53+
template() {
54+
# Setup
55+
BASE="$(mktemp --directory)"
56+
NAME="$(basename "$BASE")"
57+
if [ -z "$1" ]; then
58+
VOLATILE=false
59+
else
60+
VOLATILE=true
61+
fi
62+
docker volume create --driver docker-on-top "$NAME" -o base="$BASE" -o volatile="$VOLATILE"
63+
64+
echo 123 > "$BASE"/a
65+
echo 456 > "$BASE"/b
66+
67+
# Run the breaking action before mount
68+
$2 "$BASE" "$NAME"
69+
70+
# Start the container (might fail)
71+
if ! CONTAINER_ID=$(
72+
docker run -d -v "$NAME":/dot alpine:latest \
73+
sh -e -c "
74+
sleep 1 # Give as a moment to break everything
75+
76+
$CONTAINER_CMD_CHECK_INITIAL_DATA
77+
78+
$CONTAINER_CMD_MAKE_AND_CHECK_CHANGES
79+
"
80+
); then
81+
82+
# This is the best case: docker-on-top has detected the problem very early
83+
# and did not mount anything in the first place
84+
template_cleanup "$@"
85+
return
86+
fi
87+
88+
# Undo the breaking action
89+
$3 "$BASE" "$NAME"
90+
91+
# Run the breaking action before unmount
92+
$4 "$BASE" "$NAME"
93+
94+
if [ 0 -ne "$(docker wait "$CONTAINER_ID")" ]; then
95+
# Container has failed
96+
template_cleanup "$@"
97+
false
98+
return
99+
fi
100+
101+
# Undo the breaking action
102+
$5 "$BASE" "$NAME"
103+
104+
# Now that we are fixed, we expect that (I) either the container doesn't start at all:
105+
if ! docker run --rm -v "$NAME":/dot alpine:latest true; then
106+
template_cleanup "$@"
107+
return # The test is successful: docker-on-top detected the error and the mount is refused
108+
fi
109+
110+
# (II) Or the container starts and runs as expected:
111+
if $VOLATILE; then
112+
docker run --rm -v "$NAME":/dot alpine:latest \
113+
sh -e -c "
114+
$CONTAINER_CMD_CHECK_INITIAL_DATA
115+
116+
$CONTAINER_CMD_MAKE_AND_CHECK_CHANGES
117+
"
118+
else
119+
docker run --rm -v "$NAME":/dot alpine:latest \
120+
sh -e -c "
121+
$CONTAINER_CMD_CHECK_ALREADY_MODIFIED_DATA
122+
123+
$CONTAINER_CMD_MAKE_AND_CHECK_CHANGES
124+
"
125+
fi
126+
127+
template_cleanup "$@"
128+
}
129+
130+
131+
# I use "activate"/"deactivate" as shorts for "create/remove a file at `activemountsdir`"
132+
133+
# Break/unbreak functions receive $BASE as $1 and $NAME as $2
134+
135+
break_deactivate() {
136+
# Immutable file can not be removed
137+
sudo chattr +i /var/lib/docker-on-top/"$2"/activemounts/*
138+
}
139+
140+
unbreak_deactivate() {
141+
sudo chattr -i /var/lib/docker-on-top/"$2"/activemounts/*
142+
}
143+
144+
145+
@test "Mount=fine, activate=fine, unmount=fine, deactivate=broken" {
146+
template "" : : break_deactivate unbreak_deactivate
147+
148+
template "volatile" : : break_deactivate unbreak_deactivate
149+
}
150+
151+
break_unmount() {
152+
# The mountpoint will be kept busy with the following process that has a file on it open
153+
sleep 1.5 < /var/lib/docker-on-top/"$2"/mountpoint/b &
154+
}
155+
156+
unbreak_unmount() {
157+
# Just wait for the previous process to finish
158+
sleep 1.5
159+
}
160+
161+
162+
@test "Mount=fine, activate=fine, unmount=broken, deactivate=fine" {
163+
skip "This is not working yet, needs to be fixed"
164+
165+
template "" : : break_unmount unbreak_unmount
166+
167+
template "volatile" : : break_unmount unbreak_unmount
168+
}
169+
170+
break_activate() {
171+
sudo chattr +i /var/lib/docker-on-top/"$2"/activemounts
172+
}
173+
174+
unbreak_activate() {
175+
sudo chattr -i /var/lib/docker-on-top/"$2"/activemounts
176+
}
177+
178+
@test "Mount=fine, activate=broken, unmount=fine, deactivate=fine" {
179+
template "" break_activate unbreak_activate : :
180+
181+
template "volatile" break_activate unbreak_activate : :
182+
}
183+
184+
break_mount() {
185+
# If the base directory does not exist, the overlayfs mount shall fail
186+
rm -r "$1"
187+
}
188+
189+
unbreak_mount() {
190+
mkdir "$1"
191+
}
192+
193+
@test "Mount=broken, activate=fine, unmount=fine, deactivate=fine" {
194+
template "" break_mount unbreak_mount : :
195+
196+
template "volatile" break_mount unbreak_mount : :
197+
}

0 commit comments

Comments
 (0)