mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 02:12:12 +00:00
- Log and continue when the helper refuses a mountpoint, as the seed capture does - Nothing is moved in that case, so the image installs as before
134 lines
5.8 KiB
Python
134 lines
5.8 KiB
Python
import importlib.util
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
|
|
MODULE = Path(__file__).resolve().parents[1] / 'remote/oci_image_mountpoints.py'
|
|
spec = importlib.util.spec_from_file_location('image_mountpoints', MODULE)
|
|
image = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(image)
|
|
|
|
|
|
class ImageMountpointTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.root = Path(self.temp.name).resolve()
|
|
self.target = self.root / 'app/config'
|
|
self.target.mkdir(parents=True)
|
|
|
|
def normalize(self, paths=('/app/config',), mounts=()):
|
|
return image.normalize(self.root, paths, mounts)
|
|
|
|
def test_image_files_preserved_target_empty_and_metadata_unchanged(self):
|
|
(self.target / 'db').mkdir()
|
|
(self.target / 'DOCKER').touch()
|
|
data = self.target / 'defaults.json'
|
|
data.write_text('{"image-default":true}')
|
|
data.chmod(0o640)
|
|
self.target.chmod(0o750)
|
|
before = self.target.stat()
|
|
entries = self.normalize()
|
|
saved = self.root / entries[0]['saved'].lstrip('/')
|
|
self.assertEqual(list(self.target.iterdir()), [])
|
|
self.assertEqual((saved / 'defaults.json').read_bytes(), b'{"image-default":true}')
|
|
self.assertEqual((saved / 'defaults.json').stat().st_mode & 0o777, 0o640)
|
|
after = self.target.stat()
|
|
self.assertEqual((after.st_ino, after.st_uid, after.st_gid, after.st_mode),
|
|
(before.st_ino, before.st_uid, before.st_gid, before.st_mode))
|
|
self.assertEqual(saved.stat().st_uid, before.st_uid)
|
|
self.assertEqual(saved.stat().st_mode & 0o777, 0o750)
|
|
|
|
def test_empty_and_missing_targets_do_not_create_archive(self):
|
|
self.assertEqual(self.normalize(('/app/config', '/absent')), [])
|
|
self.assertFalse((self.root / image.ARCHIVE).exists())
|
|
|
|
def test_link_inside_target_is_preserved_without_following(self):
|
|
(self.target / 'link').symlink_to('/outside')
|
|
saved = self.root / self.normalize()[0]['saved'].lstrip('/')
|
|
self.assertEqual(os.readlink(saved / 'link'), '/outside')
|
|
|
|
def test_symlink_target_and_symlink_ancestor_rejected(self):
|
|
for value in ('/link', '/link/config'):
|
|
with self.subTest(value=value):
|
|
link = self.root / 'link'
|
|
link.symlink_to(self.root / 'app')
|
|
with self.assertRaises(ValueError):
|
|
self.normalize((value,))
|
|
link.unlink()
|
|
|
|
def test_all_paths_validated_before_changes(self):
|
|
(self.target / 'DOCKER').touch()
|
|
(self.root / 'unsafe').symlink_to('/tmp')
|
|
with self.assertRaises(ValueError):
|
|
self.normalize(('/app/config', '/unsafe'))
|
|
self.assertTrue((self.target / 'DOCKER').exists())
|
|
|
|
def test_attached_mounts_ancestors_and_descendants_rejected(self):
|
|
for mount in (self.target, self.root / 'app', self.target / 'db'):
|
|
with self.subTest(mount=mount), self.assertRaises(ValueError):
|
|
self.normalize(mounts=(mount,))
|
|
|
|
def test_other_mounted_volumes_are_not_touched(self):
|
|
other = self.root / 'persistent'
|
|
other.mkdir()
|
|
(other / 'proof').write_text('persistent data')
|
|
(self.target / 'DOCKER').touch()
|
|
self.normalize(mounts=(other,))
|
|
self.assertEqual((other / 'proof').read_text(), 'persistent data')
|
|
|
|
def test_host_root_mount_is_not_an_attached_guest_disk(self):
|
|
(self.target / 'DOCKER').touch()
|
|
self.assertTrue(self.normalize(mounts=('/', self.root)))
|
|
|
|
def test_archive_collision_preserves_everything(self):
|
|
(self.root / image.ARCHIVE).mkdir()
|
|
(self.target / 'proof').write_text('image data')
|
|
with self.assertRaises((ValueError, FileNotFoundError)):
|
|
self.normalize()
|
|
self.assertEqual((self.target / 'proof').read_text(), 'image data')
|
|
|
|
def test_unsafe_paths_rejected(self):
|
|
for path in ('/', 'relative', '/app/../data', '/app/./config', '/app//config',
|
|
'/app/config/', '/app\nconfig', f'/{image.ARCHIVE}/data'):
|
|
with self.subTest(path=path), self.assertRaises(ValueError):
|
|
self.normalize((path,))
|
|
|
|
def test_reused_volume_is_not_an_input_and_is_not_modified(self):
|
|
with tempfile.TemporaryDirectory() as data:
|
|
proof = Path(data) / 'live-data'
|
|
proof.write_text('existing service data')
|
|
(self.target / 'new-image-default').write_text('v2')
|
|
self.normalize()
|
|
self.assertEqual(proof.read_text(), 'existing service data')
|
|
|
|
def test_multiple_stack_mounts_can_be_prepared_independently(self):
|
|
(self.target / 'DOCKER').touch()
|
|
self.normalize()
|
|
other = self.root / 'other'
|
|
other.mkdir()
|
|
(other / 'defaults').write_text('keep')
|
|
saved = self.root / self.normalize(('/other',))[0]['saved'].lstrip('/')
|
|
self.assertEqual((saved / 'defaults').read_text(), 'keep')
|
|
self.assertEqual(list(other.iterdir()), [])
|
|
|
|
def test_already_prepared_target_is_idempotent(self):
|
|
(self.target / 'DOCKER').touch()
|
|
self.normalize()
|
|
self.assertEqual(self.normalize(), [])
|
|
|
|
|
|
def test_installers_keep_installing_when_a_mountpoint_is_refused(self):
|
|
# A refused path changes nothing, so the image installs as it did before.
|
|
remote = MODULE.parent
|
|
script = (remote / 'install_oci.sh').read_text()
|
|
call = script[script.index('oci_image_mountpoints.py'):]
|
|
self.assertTrue(call.split('\n', 2)[1].strip().startswith('|| oci_log'))
|
|
stack = (remote / 'install_generic_stack.py').read_text()
|
|
block = stack[stack.index('normalize(root, [mount'):][:400]
|
|
self.assertIn('except (OSError, ValueError)', block)
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|