Skip to content

Commit

Permalink
[systests] podman mount no-dereference: complete rewrite
Browse files Browse the repository at this point in the history
Existing test was very good, but as a multidimensional table it
was unmaintainable... and actually missed one corner case.

This version isn't much better. It's far longer, codewise. It
is a little harder to understand at first glance. It has three
uncomfortable magic conditionals. But I believe it is more
long-term maintainable: beyond the first glance, it is possible
for a human to check it for correctness. It is also extensible,
as proved by the new test cases I added.

Signed-off-by: Ed Santiago <[email protected]>
  • Loading branch information
edsantiago committed Nov 28, 2023
1 parent 8387d2d commit c664cfe
Showing 1 changed file with 149 additions and 50 deletions.
199 changes: 149 additions & 50 deletions test/system/060-mount.bats
Original file line number Diff line number Diff line change
Expand Up @@ -330,73 +330,172 @@ EOF
skip "only crun supports the no-dereference (copy-symlink) mount option"
fi

# One directory for testing relative symlinks, another for absolute ones.
rel_dir=$PODMAN_TMPDIR/rel-dir
abs_dir=$PODMAN_TMPDIR/abs-dir
mkdir $rel_dir $abs_dir

# Create random values to discrimate data in the rel/abs directory and the
# one from the image.
rel_random_host="rel_on_the_host_$(random_string 15)"
abs_random_host="abs_on_the_host_$(random_string 15)"
random_img="on_the_image_$(random_string 15)"

# Relative symlink
echo "$rel_random_host" > $rel_dir/data
ln -r -s $rel_dir/data $rel_dir/link
# Absolute symlink
echo "$abs_random_host" > $abs_dir/data
ln -s $abs_dir/data $abs_dir/link
# Contents of the file 'data' inside the container image.
declare -A datacontent=(
[img]="data file inside the IMAGE - $(random_string 15)"
)

# Purpose of the image is so "link -> data" can point to an existing
# file whether or not "data" is mounted.
dockerfile=$PODMAN_TMPDIR/Dockerfile
cat >$dockerfile <<EOF
FROM $IMAGE
RUN echo $random_img > /tmp/data
RUN mkdir /mountroot && echo ${datacontent[img]} > /mountroot/data
EOF

img="localhost/preserve:symlinks"
run_podman build -t $img -f $dockerfile

link_path="/tmp/link"
create_path="/tmp/i/do/not/exist/link"

# Each test is set up in exactly the same way:
#
# <tmpdir>/
# ├── mountdir/ <----- this is always the source dir
# │ ├── data
# │ └── link -> ?????
# └── otherdir/
# └── data
#
# The test is run in a container that has its own /mountroot/data file,
# so in some situations 'link -> data' will get the container's
# data file, in others it'll be the host's, and in others, ENOENT.
#
# There are four options for 'link': -> data in mountdir (same dir)
# or otherdir, and, relative or absolute. Then, for each of those
# permutations, run with and without no-dereference. (With no-dereference,
# only the first of these options is valid, link->data. The other three
# appear in the container as link->path-not-in-container)
#
# Finally, the table below defines a number of variations of mount
# type (bind, glob); mount source (just the link, a glob, or entire
# directory); and mount destination. These are the variations that
# introduce complexity, hence the special cases in the innermost loop.
#
# Table format:
#
# mount type | mount source | mount destination | what_is_data | enoents
#
# The what_is_data column indicates whether the file "data" in the
# container will be the image's copy ("img") or the one from the host
# ("in", referring to the source directory). "-" means N/A, no data file.
#
# The enoent column is a space-separated list of patterns to search for
# in the test description. When these match, "link" will point to a
# path that does not exist in the directory, and we should expect cat
# to result in ENOENT.
#
tests="
0 | bind | $rel_dir/link | /tmp/link | | /tmp/link | $rel_random_host | $link_path | bind mount relative symlink: mounts target from the host
0 | bind | $abs_dir/link | /tmp/link | | /tmp/link | $abs_random_host | $link_path | bind mount absolute symlink: mounts target from the host
0 | glob | $rel_dir/lin* | /tmp/ | | /tmp/link | $rel_random_host | $link_path | glob mount relative symlink: mounts target from the host
0 | glob | $abs_dir/lin* | /tmp/ | | /tmp/link | $abs_random_host | $link_path | glob mount absolute symlink: mounts target from the host
0 | glob | $rel_dir/* | /tmp/ | | /tmp/link | $rel_random_host | $link_path | glob mount entire directory: mounts relative target from the host
0 | glob | $abs_dir/* | /tmp/ | | /tmp/link | $abs_random_host | $link_path | glob mount entire directory: mounts absolute target from the host
0 | bind | $rel_dir/link | /tmp/link | ,no-dereference | '/tmp/link' -> 'data' | $random_img | $link_path | no_deref: bind mount relative symlink: points to file on the image
0 | glob | $rel_dir/lin* | /tmp/ | ,no-dereference | '/tmp/link' -> 'data' | $random_img | $link_path | no_deref: glob mount relative symlink: points to file on the image
0 | bind | $rel_dir/ | /tmp/ | ,no-dereference | '/tmp/link' -> 'data' | $rel_random_host | $link_path | no_deref: bind mount the entire directory: preserves symlink automatically
0 | glob | $rel_dir/* | /tmp/ | ,no-dereference | '/tmp/link' -> 'data' | $rel_random_host | $link_path | no_deref: glob mount the entire directory: preserves symlink automatically
1 | bind | $abs_dir/link | /tmp/link | ,no-dereference | '/tmp/link' -> '$abs_dir/data' | cat: can't open '/tmp/link': No such file or directory | $link_path | bind mount *preserved* absolute symlink: now points to a non-existent file on the container
1 | glob | $abs_dir/lin* | /tmp/ | ,no-dereference | '/tmp/link' -> '$abs_dir/data' | cat: can't open '/tmp/link': No such file or directory | $link_path | glob mount *preserved* absolute symlink: now points to a non-existent file on the container
0 | bind | $rel_dir/link | $create_path | | $create_path | $rel_random_host | $create_path | bind mount relative symlink: creates dirs and mounts target from the host
1 | bind | $rel_dir/link | $create_path | ,no-dereference | '$create_path' -> 'data' | cat: can't open '$create_path': No such file or directory | $create_path | no_deref: bind mount relative symlink: creates dirs and mounts target from the host
bind | /link | /mountroot/link | img
bind | /link | /i/do/not/exist/link | - | relative.*no-dereference
bind | / | /mountroot/ | in | absolute out
glob | /lin* | /mountroot/ | img
glob | /* | /mountroot/ | in
"

while read exit_code mount_type mount_src mount_dst mount_opts line_0 line_1 path description; do
if [[ $mount_opts == "''" ]];then
unset mount_opts
fi
run_podman $exit_code run \
--mount type=$mount_type,src=$mount_src,dst=$mount_dst$mount_opts \
--rm --privileged $img sh -c "stat -c '%N' $path; cat $path"
assert "${lines[0]}" = "$line_0" "$description"
assert "${lines[1]}" = "$line_1" "$description"
defer-assertion-failures

while read mount_type mount_source mount_dest what_is_data enoents; do
# link pointing inside the same directory, or outside
for in_out in "in" "out"; do
# relative symlink or absolute
for rel_abs in "relative" "absolute"; do
# Generate fresh new content for each data file (the in & out ones)
datacontent[in]="data file in the SAME DIRECTORY - $(random_string 15)"
datacontent[out]="data file OUTSIDE the tree - $(random_string 15)"

# Populate data files in and out our tree
local condition="${rel_abs:0:3}-${in_out}"
local sourcedir="$PODMAN_TMPDIR/$condition"
rm -rf $sourcedir $PODMAN_TMPDIR/outside-the-tree
mkdir $sourcedir $PODMAN_TMPDIR/outside-the-tree
echo "${datacontent[in]}" > "$sourcedir/data"
echo "${datacontent[out]}" > "$PODMAN_TMPDIR/outside-the-tree/data"

# Create the symlink itself (in the in-dir of course)
local target
case "$condition" in
rel-in) target="data" ;;
rel-out) target="../outside-the-tree/data" ;;
abs-in) target="$sourcedir/data" ;;
abs-out) target="$PODMAN_TMPDIR/outside-the-tree/data" ;;
*) die "Internal error, invalid condition '$condition'" ;;
esac
ln -s $target "$sourcedir/link"

# Absolute path to 'link' inside the container. What we stat & cat.
local containerpath="$mount_dest"
if [[ ! $containerpath =~ /link$ ]]; then
containerpath="${containerpath}link"
fi

# Now test with no args (mounts link CONTENT) and --no-dereference
# (mounts symlink AS A SYMLINK)
for mount_opts in "" ",no-dereference"; do
local description="$mount_type mount $mount_source -> $mount_dest ($in_out), $rel_abs $mount_opts"

# Expected exit status. Almost always success.
local exit_code=0

# Without --no-dereference, we always expect exactly the same,
# because podman mounts "link" as a data file...
local expect_stat="$containerpath"
local expect_cat="${datacontent[$in_out]}"
# ...except when bind-mounting link's parent directory: "link"
# is mounted as a link, and host's "data" file overrides the image
if [[ $mount_source = '/' ]]; then
expect_stat="'$containerpath' -> '$target'"
fi

# With --no-dereference...
if [[ -n "$mount_opts" ]]; then
# stat() is always the same (symlink and its target) ....
expect_stat="'$containerpath' -> '$target'"

# ...and the only valid case for cat is same-dir relative:
if [[ "$condition" = "rel-in" ]]; then
expect_cat="${datacontent[$what_is_data]}"
else
# All others are ENOENT, because link -> nonexistent-path
exit_code=1
fi
fi

for ex in $enoents; do
if grep -q -w -E "$ex" <<<"$description"; then
exit_code=1
fi
done
if [[ $exit_code -eq 1 ]]; then
expect_cat="cat: can't open '$containerpath': No such file or directory"
fi

run_podman $exit_code run \
--mount type=$mount_type,src="$sourcedir$mount_source",dst="$mount_dest$mount_opts" \
--rm --privileged $img sh -c "stat -c '%N' $containerpath; cat $containerpath"
assert "${lines[0]}" = "$expect_stat" "$description -- stat $containerpath"
assert "${lines[1]}" = "$expect_cat" "$description -- cat $containerpath"
done
done
done
done < <(parse_table "$tests")

# Make sure that it's presvered across starts and stops
run_podman create --mount type=glob,src=$rel_dir/*,dst=/tmp/,no-dereference --privileged $img sh -c "stat -c '%N' /tmp/link; cat /tmp/link"
immediate-assertion-failures

# Make sure that links are preserved across starts and stops
local workdir=$PODMAN_TMPDIR/test-restart
mkdir $workdir
local datafile="data-$(random_string 5)"
local datafile_contents="What we expect to see, $(random_string 20)"
echo "$datafile_contents" > $workdir/$datafile
ln -s $datafile $workdir/link

run_podman create --mount type=glob,src=$workdir/*,dst=/mountroot/,no-dereference --privileged $img sh -c "stat -c '%N' /mountroot/link; cat /mountroot/link"
cid="$output"
run_podman start -a $cid
assert "${lines[0]}" = "'/tmp/link' -> 'data'" "symlink is preserved"
assert "${lines[1]}" = "$rel_random_host" "glob macthes symlink and host 'data' file"
assert "${lines[0]}" = "'/mountroot/link' -> '$datafile'" "symlink is preserved, on start"
assert "${lines[1]}" = "$datafile_contents" "glob matches symlink and host 'data' file, on start"
run_podman start -a $cid
assert "${lines[0]}" = "'/tmp/link' -> 'data'" "symlink is preserved"
assert "${lines[1]}" = "$rel_random_host" "glob macthes symlink and host 'data' file"
assert "${lines[0]}" = "'/mountroot/link' -> '$datafile'" "symlink is preserved, on restart"
assert "${lines[1]}" = "$datafile_contents" "glob matches symlink and host 'data' file, on restart"
run_podman rm -f -t=0 $cid

run_podman rmi -f $img
Expand Down

0 comments on commit c664cfe

Please sign in to comment.