def test_symlink(self): self._check_item( SymlinkToDirItem(from_target='t', source='x', dest='y'), {ProvidesDirectory(path='y')}, {require_directory('/'), require_directory('/x')}, ) self._check_item( SymlinkToFileItem(from_target='t', source='source_file', dest='dest_symlink'), {ProvidesFile(path='dest_symlink')}, {require_directory('/'), require_file('/source_file')}, )
def test_make_dirs(self): self._check_item( MakeDirsItem(from_target='t', into_dir='x', path_to_make='y/z'), {ProvidesDirectory(path='x/y'), ProvidesDirectory(path='x/y/z')}, {require_directory('x')}, )
def test_tarball(self): with temp_filesystem() as fs_path, tempfile.TemporaryDirectory() as td: tar_path = os.path.join(td, 'test.tar') zst_path = os.path.join(td, 'test.tar.zst') with tarfile.TarFile(tar_path, 'w') as tar_obj: tar_obj.add(fs_path, filter=_tarinfo_strip_dir_prefix(fs_path)) subprocess.check_call(['zstd', tar_path, '-o', zst_path]) for path in (tar_path, zst_path): self._check_item( _tarball_item(path, 'y'), temp_filesystem_provides('y'), {require_directory('y')}, ) # Test a hash validation failure, follows the item above with self.assertRaisesRegex(AssertionError, 'failed hash vali'): image_source_item( TarballItem, exit_stack=None, layer_opts=DUMMY_LAYER_OPTS, )( from_target='t', into_dir='y', source={ 'source': tar_path, 'content_hash': 'sha256:deadbeef', }, force_root_ownership=False, )
def test_clone_special_files(self): with TempSubvolumes(sys.argv[0]) as temp_subvols: src_subvol = temp_subvols.create('test_clone_special_files_src') dest_subvol = temp_subvols.create('test_clone_special_files_dest') src_subvol.run_as_root(['mkfifo', src_subvol.path('fifo')]) src_subvol.run_as_root([ 'mknod', src_subvol.path('null'), 'c', '1', '3', ]) for name in ['fifo', 'null']: ci = self._clone_item(name, name, subvol=src_subvol) self.assertEqual(name, ci.dest) self._check_item( ci, {ProvidesFile(path=name)}, {require_directory('/')}, ) ci.build(dest_subvol, DUMMY_LAYER_OPTS) src_r = render_subvol(src_subvol) dest_r = render_subvol(dest_subvol) self.assertEqual(src_r, dest_r) self.assertEqual( ['(Dir)', { 'fifo': ['(FIFO)'], 'null': ['(Char 103)'], }], dest_r)
def test_tarball_generator(self): with temp_filesystem() as fs_path, tempfile.NamedTemporaryFile() as t, \ ExitStack() as exit_stack: with tarfile.TarFile(t.name, 'w') as tar_obj: tar_obj.add(fs_path, filter=_tarinfo_strip_dir_prefix(fs_path)) self._check_item( image_source_item( TarballItem, exit_stack=exit_stack, layer_opts=DUMMY_LAYER_OPTS, )( from_target='t', into_dir='y', source={ 'generator': '/bin/bash', 'generator_args': [ '-c', 'cp "$1" "$2"; basename "$1"', 'test_tarball_generator', # $0 t.name, # $1, making $2 the output directory ], 'content_hash': 'sha256:' + _hash_path(t.name, 'sha256'), }, force_root_ownership=False, ), temp_filesystem_provides('y'), {require_directory('y')}, )
def test_clone_nonexistent_source(self): ci = self._clone_item('/no_such_path', '/none_such') self.assertEqual('none_such', ci.dest) with self.assertRaises(subprocess.CalledProcessError): self._check_item(ci, set(), {require_directory('/')}) with TempSubvolumes(sys.argv[0]) as temp_subvols: subvol = temp_subvols.create('test_clone_nonexistent_source') with self.assertRaises(subprocess.CalledProcessError): ci.build(subvol, DUMMY_LAYER_OPTS)
def test_install_file(self): with tempfile.NamedTemporaryFile() as tf: os.chmod(tf.name, stat.S_IXUSR) exe_item = _install_file_item( from_target='t', source={'source': tf.name}, dest='d/c', ) ep = _InstallablePath(Path(tf.name), ProvidesFile(path='d/c'), 'a+rx') self.assertEqual((ep, ), exe_item.paths) self.assertEqual(tf.name.encode(), exe_item.source) self._check_item(exe_item, {ep.provides}, {require_directory('d')}) # Checks `image.source(path=...)` with temp_dir() as td: os.mkdir(td / 'b') open(td / 'b/q', 'w').close() data_item = _install_file_item( from_target='t', source={ 'source': td, 'path': '/b/q' }, dest='d', ) dp = _InstallablePath(td / 'b/q', ProvidesFile(path='d'), 'a+r') self.assertEqual((dp, ), data_item.paths) self.assertEqual(td / 'b/q', data_item.source) self._check_item(data_item, {dp.provides}, {require_directory('/')}) # NB: We don't need to get coverage for this check on ALL the items # because the presence of the ProvidesDoNotAccess items it the real # safeguard -- e.g. that's what prevents TarballItem from writing # to /meta/ or other protected paths. with self.assertRaisesRegex(AssertionError, 'cannot start with meta/'): _install_file_item( from_target='t', source={'source': 'a/b/c'}, dest='/meta/foo', )
def test_stat_options(self): self._check_item( MakeDirsItem( from_target='t', into_dir='x', path_to_make='y/z', mode=0o733, user_group='cat:dog', ), {ProvidesDirectory(path='x/y'), ProvidesDirectory(path='x/y/z')}, {require_directory('x')}, )
def test_clone_file(self): ci = self._clone_item('/rpm_test/hello_world.tar', '/cloned_hello.tar') self.assertEqual('cloned_hello.tar', ci.dest) self._check_item( ci, {ProvidesFile(path='cloned_hello.tar')}, {require_directory('/')}, ) with TempSubvolumes(sys.argv[0]) as temp_subvols: subvol = temp_subvols.create('test_clone_file') ci.build(subvol, DUMMY_LAYER_OPTS) r = render_subvol(subvol) ino, = pop_path(r, 'cloned_hello.tar') self.assertRegex(ino, '(File m444 d[0-9]+)') self.assertEqual(['(Dir)', {}], r)
def test_clone_pre_existing_dest(self): ci = self._clone_item('/foo/bar', '/', pre_existing_dest=True) self.assertEqual('', ci.dest) self._check_item( ci, { ProvidesDirectory(path='bar'), ProvidesDirectory(path='bar/baz'), ProvidesFile(path='bar/baz/bar'), ProvidesFile(path='bar/even_more_hello_world.tar'), }, {require_directory('/')}, ) with TempSubvolumes(sys.argv[0]) as temp_subvols: subvol = temp_subvols.create('test_clone_pre_existing_dest') self._check_clone_bar(ci, subvol)
def test_clone_hardlinks(self): with TempSubvolumes(sys.argv[0]) as temp_subvols: src_subvol = temp_subvols.create('test_clone_hardlinks_src') dest_subvol = temp_subvols.create('test_clone_hardlinks_dest') src_subvol.run_as_root(['touch', src_subvol.path('a')]) src_subvol.run_as_root([ 'ln', src_subvol.path('a'), src_subvol.path('b'), ]) ci = self._clone_item( '/', '/', omit_outer_dir=True, pre_existing_dest=True, subvol=src_subvol, ) self.assertEqual('', ci.dest) self._check_item( ci, { ProvidesFile(path='a'), ProvidesFile(path='b'), # This looks like a bug (there's no /meta on disk here) but # it's really just an artifact of how this path is # protected. Read: This Is Fine (TM). ProvidesDoNotAccess(path='/meta'), }, {require_directory('/')}) ci.build(dest_subvol, DUMMY_LAYER_OPTS) src_r = render_subvol(src_subvol) dest_r = render_subvol(dest_subvol) self.assertEqual(src_r, dest_r) self.assertEqual( [ '(Dir)', { # Witness that they have the same (rendered) inode # of "0" 'a': [['(File)', 0]], 'b': [['(File)', 0]], } ], dest_r)
def test_install_file_from_layer(self): layer = find_built_subvol( Path(__file__).dirname() / 'test-with-one-local-rpm') path_in_layer = b'rpm_test/cheese2.txt' item = _install_file_item( from_target='t', source={ 'layer': layer, 'path': '/' + path_in_layer.decode() }, dest='cheese2', ) source_path = layer.path(path_in_layer) p = _InstallablePath(source_path, ProvidesFile(path='cheese2'), 'a+r') self.assertEqual((p, ), item.paths) self.assertEqual(source_path, item.source) self._check_item(item, {p.provides}, {require_directory('/')})
def test_clone_demo_sendstream(self): src_subvol = layer_resource_subvol(__package__, 'create_ops') ci = self._clone_item( '/', '/', omit_outer_dir=True, pre_existing_dest=True, subvol=src_subvol, ) self.assertEqual({require_directory('/')}, set(ci.requires())) self.assertGreater(len(set(ci.provides())), 1) with TempSubvolumes(sys.argv[0]) as temp_subvols: dest_subvol = temp_subvols.create('create_ops') ci.build(dest_subvol, DUMMY_LAYER_OPTS) self.assertEqual( render_subvol(src_subvol), render_subvol(dest_subvol), )
def test_clone_omit_outer_dir(self): ci = self._clone_item( '/foo/bar', '/bar', omit_outer_dir=True, pre_existing_dest=True, ) self.assertEqual('bar', ci.dest) self._check_item( ci, { ProvidesDirectory(path='bar/baz'), ProvidesFile(path='bar/baz/bar'), ProvidesFile(path='bar/even_more_hello_world.tar'), }, {require_directory('/bar')}, ) with TempSubvolumes(sys.argv[0]) as temp_subvols: subvol = temp_subvols.create('test_clone_omit_outer_dir') subvol.run_as_root(['mkdir', subvol.path('bar')]) self._check_clone_bar(ci, subvol)
def test_install_file_command_recursive(self): with TempSubvolumes(sys.argv[0]) as temp_subvolumes: subvol = temp_subvolumes.create('tar-sv') subvol.run_as_root(['mkdir', subvol.path('d')]) with temp_dir() as td: with open(td / 'data.txt', 'w') as df: print('Hello', file=df) os.mkdir(td / 'subdir') with open(td / 'subdir/exe.sh', 'w') as ef: print('#!/bin/sh\necho "Hello"', file=ef) os.chmod(td / 'subdir/exe.sh', 0o100) dir_item = _install_file_item( from_target='t', source={'source': td}, dest='/d/a', ) ps = [ _InstallablePath( td, ProvidesDirectory(path='d/a'), 'u+rwx,og+rx', ), _InstallablePath( td / 'data.txt', ProvidesFile(path='d/a/data.txt'), 'a+r', ), _InstallablePath( td / 'subdir', ProvidesDirectory(path='d/a/subdir'), 'u+rwx,og+rx', ), _InstallablePath( td / 'subdir/exe.sh', ProvidesFile(path='d/a/subdir/exe.sh'), 'a+rx', ), ] self.assertEqual(sorted(ps), sorted(dir_item.paths)) self.assertEqual(td, dir_item.source) self._check_item(dir_item, {p.provides for p in ps}, {require_directory('d')}) # This implicitly checks that `a` precedes its contents. dir_item.build(subvol, DUMMY_LAYER_OPTS) self.assertEqual( [ '(Dir)', { 'd': [ '(Dir)', { 'a': [ '(Dir)', { 'data.txt': ['(File m444 d6)'], 'subdir': [ '(Dir)', { 'exe.sh': ['(File m555 d23)'], } ], } ] } ] } ], render_subvol(subvol), )
def requires(self): # We don't require the mountpoint itself since it will be shadowed, # so this item just makes it with default permissions. yield require_directory(os.path.dirname(self.mountpoint))
def requires(self): if not _whitelisted_symlink_source(self.source): yield require_file(self.source) yield require_directory(os.path.dirname(self.dest))
def test_mount_item(self): with TempSubvolumes(sys.argv[0]) as temp_subvolumes, \ tempfile.TemporaryDirectory() as source_dir: runtime_source = {'so': 'me', 'arbitrary': {'j': 'son'}} mount_config = { 'is_directory': True, 'build_source': { 'type': 'layer', 'source': '//fake:path' }, 'runtime_source': runtime_source, } with open(os.path.join(source_dir, 'mountconfig.json'), 'w') as f: json.dump(mount_config, f) self._check_item( self._make_mount_item( mountpoint='can/haz', target=source_dir, mount_config=mount_config, ), {ProvidesDoNotAccess(path='can/haz')}, {require_directory('can')}, ) # Make a subvolume that we will mount inside `mounter` mountee = temp_subvolumes.create('moun:tee/volume') mountee.run_as_root(['tee', mountee.path('kitteh')], input=b'cheez') # These sub-mounts inside `mountee` act as canaries to make sure # that (a) `mounter` receives the sub-mounts as a consequence of # mounting `mountee` recursively, (b) that unmounting one in # `mounter` does not affect the original in `mountee` -- i.e. # that rslave propagation is set up correctly, (c) that # unmounting in `mountee` immediately affects `mounter`. # # In practice, our build artifacts should NEVER be mutated after # construction (and the only un-mount is implicitly, and # seemingly safely, performed by `btrfs subvolume delete`). # However, ensuring that we have correct `rslave` propagation is # a worthwhile safeguard for host mounts, where an errant # `umount` by a user inside their repo could otherwise break # their host. for submount in ('submount1', 'submount2'): mountee.run_as_root(['mkdir', mountee.path(submount)]) mountee.run_as_root([ 'mount', '-o', 'bind,ro', source_dir, mountee.path(submount) ]) self.assertTrue( os.path.exists(mountee.path(submount + '/mountconfig.json'))) # Make the JSON file normally in "buck-out" that refers to `mountee` mountee_subvolumes_dir = self._write_layer_json_into( mountee, source_dir) # Mount <mountee> at <mounter>/meow mounter = temp_subvolumes.caller_will_create('mount:er/volume') root_item = FilesystemRootItem(from_target='t') root_item.get_phase_builder([root_item], DUMMY_LAYER_OPTS)(mounter) mount_meow = self._make_mount_item( mountpoint='meow', target=source_dir, mount_config=mount_config, ) self.assertEqual( runtime_source, json.loads(mount_meow.runtime_source), ) with self.assertRaisesRegex(AssertionError, ' could not resolve '): mount_meow.build_source.to_path( target_to_path={}, subvolumes_dir=mountee_subvolumes_dir, ) mount_meow.build( mounter, DUMMY_LAYER_OPTS._replace( target_to_path={'//fake:path': source_dir}, subvolumes_dir=mountee_subvolumes_dir, )) # This checks the subvolume **contents**, but not the mounts. # Ensure the build created a mountpoint, and populated metadata. self._check_subvol_mounts_meow(mounter) # `mountee` was also mounted at `/meow` with open(mounter.path('meow/kitteh')) as f: self.assertEqual('cheez', f.read()) def check_mountee_mounter_submounts(submount_presence): for submount, (in_mountee, in_mounter) in submount_presence: self.assertEqual( in_mountee, os.path.exists( mountee.path(submount + '/mountconfig.json')), f'{submount}, {in_mountee}') self.assertEqual( in_mounter, os.path.exists( mounter.path('meow/' + submount + '/mountconfig.json')), f'{submount}, {in_mounter}') # Both sub-mounts are accessible in both places now. check_mountee_mounter_submounts([ ('submount1', (True, True)), ('submount2', (True, True)), ]) # Unmounting `submount1` from `mountee` also affects `mounter`. mountee.run_as_root(['umount', mountee.path('submount1')]) check_mountee_mounter_submounts([ ('submount1', (False, False)), ('submount2', (True, True)), ]) # Unmounting `submount2` from `mounter` doesn't affect `mountee`. mounter.run_as_root(['umount', mounter.path('meow/submount2')]) check_mountee_mounter_submounts([ ('submount1', (False, False)), ('submount2', (True, False)), ]) # Check that we read back the `mounter` metadata, mark `/meow` # inaccessible, and do not emit a `ProvidesFile` for `kitteh`. self._check_item( PhasesProvideItem(from_target='t', subvol=mounter), { ProvidesDirectory(path='/'), ProvidesDoNotAccess(path='/meta'), ProvidesDoNotAccess(path='/meow'), }, set(), ) # Check that we successfully clone mounts from the parent layer. mounter_child = temp_subvolumes.caller_will_create('child/volume') ParentLayerItem.get_phase_builder( [ParentLayerItem(from_target='t', subvol=mounter)], DUMMY_LAYER_OPTS, )(mounter_child) # The child has the same mount, and the same metadata self._check_subvol_mounts_meow(mounter_child) # Check that we refuse to create nested mounts. nested_mounter = temp_subvolumes.create('nested_mounter') nested_item = MountItem( layer_opts=DUMMY_LAYER_OPTS, from_target='t', mountpoint='/whatever', target=None, mount_config={ 'is_directory': True, 'build_source': { 'type': 'layer', 'source': '//:fake' }, }, ) with tempfile.TemporaryDirectory() as d: mounter_subvolumes_dir = self._write_layer_json_into( mounter, d) with self.assertRaisesRegex( AssertionError, 'Refusing .* nested mount', ): nested_item.build( nested_mounter, DUMMY_LAYER_OPTS._replace( target_to_path={'//:fake': d}, subvolumes_dir=mounter_subvolumes_dir, ))
def requires(self): yield require_directory(self.into_dir)
def requires(self): yield require_directory(self.dest if self.pre_existing_dest else os. path.dirname(self.dest))
def requires(self): yield require_directory(self.source) yield require_directory(os.path.dirname(self.dest))