ostree/tests/kolainst/destructive/state-overlay.sh
Jonathan Lebon 92b1a27202 Add concept of state overlays
In the OSTree model, executables go in `/usr`, state in `/var` and
configuration in `/etc`. Software that lives in `/opt` however messes
this up because it often mixes code *and* state, making it harder to
manage.

More generally, it's sometimes useful to have the OSTree commit contain
code under a certain path, but still allow that path to be writable by
software and the sysadmin at runtime (`/usr/local` is another instance).

Add the concept of state overlays. A state overlay is an overlayfs
mount whose upper directory, which contains unmanaged state, is carried
forward on top of a lower directory, containing OSTree-managed files.

In the example of `/usr/local`, OSTree commits can ship content there,
all while allowing users to e.g. add scripts in `/usr/local/bin` when
booted into that commit.

Some reconciliation logic is executed whenever the base is updated so
that newer files in the base are never shadowed by a copied up version
in the upper directory. This matches RPM semantics when upgrading
packages whose files may have been modified.

For ease of integration, this is exposed as a systemd template unit which
any downstream distro/user can enable. The instance name is the mountpath
in escaped systemd path notation (e.g.
`ostree-state-overlay@usr-local.service`).

See discussions in https://github.com/ostreedev/ostree/issues/3113 for
more details.
2024-01-09 23:20:41 -05:00

147 lines
4.5 KiB
Bash
Executable File

#!/bin/bash
set -xeuo pipefail
. ${KOLA_EXT_DATA}/libinsttest.sh
case "${AUTOPKGTEST_REBOOT_MARK:-}" in
"")
# create a new ostree commit with some toplevel content
mkdir -p /var/tmp/rootfs/foobar
(cd /var/tmp/rootfs/foobar
touch an_empty_file
echo 'foobar' > a_non_empty_file
echo 'foobar' > another_file
ln -s an_empty_file a_working_symlink
ln -s enoent a_broken_symlink
mkdir an_empty_subdir
mkdir a_nonempty_subdir
echo foobar > a_nonempty_subdir/foobar
mkdir -p a_deeply/deeply/nested/subdir
echo foobar > a_deeply/deeply/nested/subdir/foobar
# test content deletion
mkdir a_dir_to_delete
touch a_file_to_delete
ln -s enoent a_symlink_to_delete
# opaque directory
mkdir a_dir_to_make_opaque
touch a_dir_to_make_opaque/base
)
ostree commit --no-bindings -P -b foobar --tree=ref="${host_commit}" --tree=dir=/var/tmp/rootfs
rpm-ostree rebase :foobar
systemctl enable ostree-state-overlay@foobar.service
/tmp/autopkgtest-reboot "2"
;;
"2")
if ! test -d /foobar; then
fatal "no /foobar toplevel dir"
fi
if [[ $(findmnt /foobar -no SOURCE) != overlay ]]; then
fatal "/foobar is not overlay"
fi
cd /foobar
# create some state files (i.e. not shadowing)
echo "state" > state
echo "state" > a_nonempty_subdir/state
echo "state" > a_deeply/deeply/nested/subdir/state
ln -s foobar state_symlink
mkdir state_dir
# and shadow some base files
# make empty file non-empty
echo shadow > an_empty_file
# make a file become a directory
rm a_non_empty_file && mkdir a_non_empty_file
# make a file become a symlink
ln -sf some_target another_file
# override a working symlink
ln -sf another_file a_working_symlink
# override a non-working symlink
ln -sf enoent2 a_broken_symlink
# make dir become a file
rmdir an_empty_subdir
touch an_empty_subdir
# override file in a shallow subdir
echo shadow > a_nonempty_subdir/foobar
# override file in a deep subdir
echo shadow > a_deeply/deeply/nested/subdir/foobar
# delete some base files
rmdir a_dir_to_delete
rm a_file_to_delete
rm a_symlink_to_delete
# opaque directory
rm -rf a_dir_to_make_opaque
mkdir a_dir_to_make_opaque
touch a_dir_to_make_opaque/state
# check that rebooting without upgrading maintains state
/tmp/autopkgtest-reboot "3"
;;
"3")
cd /foobar
# check state is still there
assert_file_has_content state state
assert_file_has_content a_nonempty_subdir/state state
assert_file_has_content a_deeply/deeply/nested/subdir/state state
[[ $(readlink state_symlink) == foobar ]]
test -d state_dir
# check shadowings
assert_file_has_content an_empty_file shadow
test -d a_non_empty_file
[[ $(readlink another_file) == some_target ]]
[[ $(readlink a_working_symlink) == another_file ]]
[[ $(readlink a_broken_symlink) == enoent2 ]]
test -f an_empty_subdir
assert_file_has_content a_nonempty_subdir/foobar shadow
assert_file_has_content a_deeply/deeply/nested/subdir/foobar shadow
! test -e a_dir_to_delete
! test -e a_file_to_delete
! test -e a_symlink_to_delete
# opaque directory
test -d a_dir_to_make_opaque
! test -e a_dir_to_make_opaque/base
test -e a_dir_to_make_opaque/state
# now reboot into an upgrade
ostree commit --no-bindings -P -b foobar --tree=ref="${host_commit}"
rpm-ostree upgrade
/tmp/autopkgtest-reboot "4"
;;
"4")
cd /foobar
# check state is still there
assert_file_has_content state state
assert_file_has_content a_nonempty_subdir/state state
assert_file_has_content a_deeply/deeply/nested/subdir/state state
[[ $(readlink state_symlink) == foobar ]]
test -d state_dir
# check shadowings are gone
test -f an_empty_file
assert_file_has_content a_non_empty_file foobar
assert_file_has_content another_file foobar
[[ $(readlink a_working_symlink) == an_empty_file ]]
[[ $(readlink a_broken_symlink) == enoent ]]
test -d an_empty_subdir
test -d a_nonempty_subdir
assert_file_has_content a_nonempty_subdir/foobar foobar
assert_file_has_content a_deeply/deeply/nested/subdir/foobar foobar
test -d a_dir_to_delete
test -f a_file_to_delete
test -L a_symlink_to_delete
# opaque directory
test -d a_dir_to_make_opaque
test -e a_dir_to_make_opaque/base
! test -e a_dir_to_make_opaque/state
;;
*) fatal "Unexpected AUTOPKGTEST_REBOOT_MARK=${AUTOPKGTEST_REBOOT_MARK}" ;;
esac