예제 #1
0
 def test_config(self):
     with self.assertRaisesRegex(AssertionError, r'expect non-empty'):
         models.PodConfig(
             name='test-pod',
             version='0.0.1',
             apps=self.sample_config.apps,
             images=[],
         )
     with self.assertRaisesRegex(
         AssertionError, r'expect unique elements in '
     ):
         models.PodConfig(
             name='test-pod',
             version='0.0.1',
             apps=[
                 models.PodConfig.App(name='some-app', exec=['/bin/true']),
                 models.PodConfig.App(name='some-app', exec=['/bin/false']),
             ],
             images=self.sample_config.images,
         )
     with self.assertRaisesRegex(
         AssertionError, r'expect unique elements in '
     ):
         models.PodConfig(
             name='test-pod',
             version='0.0.1',
             apps=self.sample_config.apps,
             images=self.sample_config.images,
             mounts=[
                 models.PodConfig.Mount(source='/p', target='/a'),
             ],
             overlays=[
                 models.PodConfig.Overlay(sources=['/q'], target='/a'),
             ],
         )
     with self.assertRaisesRegex(AssertionError, r'expect only one'):
         models.PodConfig.Image()
     with self.assertRaisesRegex(AssertionError, r'expect.*xor.*be false'):
         models.PodConfig.Image(name='name')
     with self.assertRaisesRegex(AssertionError, r'expect.*is_absolute'):
         models.PodConfig.Mount(source='foo', target='/bar')
     with self.assertRaisesRegex(AssertionError, r'expect.*is_absolute'):
         models.PodConfig.Mount(source='/foo', target='bar')
     with self.assertRaisesRegex(AssertionError, r'expect non-empty'):
         models.PodConfig.Overlay(sources=[], target='/bar')
     with self.assertRaisesRegex(AssertionError, r'expect.*is_absolute'):
         models.PodConfig.Overlay(sources=['foo'], target='/bar')
     with self.assertRaisesRegex(AssertionError, r'expect.*is_absolute'):
         models.PodConfig.Overlay(sources=['/foo'], target='bar')
     with self.assertRaisesRegex(AssertionError, r'expect x == 1, not 0'):
         models.PodConfig.Overlay(sources=['', '/foo'], target='/bar')
예제 #2
0
    def test_iter_image_ids(self):

        def list_image_ids(config):
            return sorted(pods._iter_image_ids(config))

        self.create_image_dir(self.sample_image_id, self.sample_metadata)
        images.cmd_tag(image_id=self.sample_image_id, new_tag='some-tag')

        config = models.PodConfig(
            name='test-pod',
            version='0.0.1',
            apps=self.sample_config.apps,
            images=[models.PodConfig.Image(id=self.sample_image_id)],
        )
        self.assertEqual(list_image_ids(config), [self.sample_image_id])

        config = models.PodConfig(
            name='test-pod',
            version='0.0.1',
            apps=self.sample_config.apps,
            images=[models.PodConfig.Image(name='sample-app', version='1.0')],
        )
        self.assertEqual(list_image_ids(config), [self.sample_image_id])

        config = models.PodConfig(
            name='test-pod',
            version='0.0.1',
            apps=self.sample_config.apps,
            images=[models.PodConfig.Image(tag='some-tag')],
        )
        self.assertEqual(list_image_ids(config), [self.sample_image_id])

        config = models.PodConfig(
            name='test-pod',
            version='0.0.1',
            apps=self.sample_config.apps,
            images=[models.PodConfig.Image(name='no-such-app', version='1.0')],
        )
        with self.assertRaisesRegex(AssertionError, r'expect non-None value'):
            list_image_ids(config)

        config = models.PodConfig(
            name='test-pod',
            version='0.0.1',
            apps=self.sample_config.apps,
            images=[models.PodConfig.Image(tag='no-such-tag')],
        )
        with self.assertRaisesRegex(AssertionError, r'expect non-None value'):
            list_image_ids(config)
예제 #3
0
 def test_not_unique_systemd_unit_names(self):
     with self.assertRaisesRegex(
             AssertionError,
             r'expect unique elements in ',
     ):
         models.PodDeployInstruction(
             label='//foo/bar:dummy',
             pod_config_template=ctr_models.PodConfig(
                 name='dummy',
                 version='0.0.1',
                 apps=[],
                 images=[
                     ctr_models.PodConfig.Image(
                         name='dummy',
                         version='0.0.1',
                     ),
                 ],
             ),
             volumes=[],
             systemd_unit_groups=[
                 models.PodDeployInstruction.SystemdUnitGroup(units=[
                     models.PodDeployInstruction.SystemdUnitGroup.Unit(
                         'foo.service', ''),
                     models.PodDeployInstruction.SystemdUnitGroup.Unit(
                         'foo.service', ''),
                 ], )
             ],
         )
예제 #4
0
 def test_not_unique_volume_names(self):
     with self.assertRaisesRegex(
             AssertionError,
             r'expect unique elements in ',
     ):
         models.PodDeployInstruction(
             label='//foo/bar:dummy',
             pod_config_template=ctr_models.PodConfig(
                 name='dummy',
                 version='0.0.1',
                 apps=[],
                 images=[
                     ctr_models.PodConfig.Image(
                         name='dummy',
                         version='0.0.1',
                     ),
                 ],
             ),
             volumes=[
                 models.PodDeployInstruction.Volume(
                     label='//a:x',
                     version='0.0.1',
                     target='/a',
                 ),
                 models.PodDeployInstruction.Volume(
                     label='//b:x',
                     version='0.0.2',
                     target='/b',
                 ),
             ],
         )
예제 #5
0
    def test_iter_ref_image_ids(self):

        def list_ref_image_ids(pod_dir_path):
            return sorted(pods._iter_ref_image_ids(pod_dir_path))

        image_id_1 = self.make_image_id(1)
        image_id_2 = self.make_image_id(2)
        self.create_pod_dir(self.sample_pod_id, self.sample_config)
        self.create_image_dir(image_id_1, self.sample_metadata)
        self.create_image_dir(image_id_2, self.sample_metadata)
        self.assertEqual(list_ref_image_ids(self.sample_pod_dir_path), [])

        pods._add_ref_image_ids(
            self.sample_pod_dir_path,
            models.PodConfig(
                name='test-pod',
                version='0.0.1',
                apps=self.sample_config.apps,
                images=[
                    models.PodConfig.Image(id=image_id_1),
                    models.PodConfig.Image(id=image_id_2),
                ],
            ),
        )
        self.assertEqual(
            list_ref_image_ids(self.sample_pod_dir_path),
            [image_id_1, image_id_2],
        )
예제 #6
0
파일: pods.py 프로젝트: clchiou/garage
def _generate_deploy_instruction(
    *,
    parameters,
    pod_dir_path,
    name,
    version,
    apps,
    images,
    mounts,
    volumes,
    systemd_unit_groups,
    token_names,
):
    releases.dump(
        ops_models.PodDeployInstruction(
            label=str(releases.get_output_label(name)),
            pod_config_template=ctr_models.PodConfig(
                name=name,
                version=version,
                apps=apps,
                images=[
                    ctr_models.PodConfig.Image(
                        name=shipyard2.BASE,
                        version=_images.get_image_version(
                            parameters,
                            shipyard2.BASE_LABEL,
                        ),
                    ),
                    *(
                        ctr_models.PodConfig.Image(
                            name=str(image.name),
                            version=_images.get_image_version(
                                parameters,
                                image,
                            ),
                        ) for image in images
                    ),
                ],
                mounts=mounts,
            ),
            volumes=volumes,
            systemd_unit_groups=systemd_unit_groups,
            token_names=token_names or {},
        ),
        pod_dir_path / shipyard2.POD_DIR_DEPLOY_INSTRUCTION_FILENAME,
    )
예제 #7
0
    def test_add_ref_image_ids(self):

        def list_image_ids():
            return sorted(
                p.name for p in \
                pods._get_deps_path(self.sample_pod_dir_path).iterdir()
            )

        image_id_1 = self.make_image_id(1)
        image_id_2 = self.make_image_id(2)
        self.create_pod_dir(self.sample_pod_id, self.sample_config)
        self.create_image_dir(image_id_1, self.sample_metadata)
        self.create_image_dir(image_id_2, self.sample_metadata)
        self.assertEqual(list_image_ids(), [])
        self.assertEqual(
            images._get_ref_count(images.get_image_dir_path(image_id_1)), 1
        )
        self.assertEqual(
            images._get_ref_count(images.get_image_dir_path(image_id_2)), 1
        )

        config = models.PodConfig(
            name='test-pod',
            version='0.0.1',
            apps=self.sample_config.apps,
            images=[
                models.PodConfig.Image(id=image_id_1),
                models.PodConfig.Image(id=image_id_2),
            ],
        )

        new_config = pods._add_ref_image_ids(self.sample_pod_dir_path, config)
        self.assertEqual(config, new_config)
        self.assertEqual(list_image_ids(), [image_id_1, image_id_2])
        self.assertEqual(
            images._get_ref_count(images.get_image_dir_path(image_id_1)), 2
        )
        self.assertEqual(
            images._get_ref_count(images.get_image_dir_path(image_id_2)), 2
        )
예제 #8
0
class PodConfigTest(unittest.TestCase):

    POD_CONFIG_TEMPLATE = ctr_models.PodConfig(
        name='dummy',
        version='0.0.1',
        apps=[
            ctr_models.PodConfig.App(
                name='foo',
                exec=['echo', '{message}'],
            ),
        ],
        images=[
            ctr_models.PodConfig.Image(
                name='some-image',
                version='0.0.2',
            ),
        ],
        mounts=[
            ctr_models.PodConfig.Mount(
                source='',
                target='/tmp',
                read_only=False,
            ),
        ],
    )

    POD_CONFIG = ctr_models.PodConfig(
        name='dummy',
        version='0.0.1',
        apps=[
            ctr_models.PodConfig.App(
                name='foo',
                exec=['echo', 'hello world'],
            ),
        ],
        images=[
            ctr_models.PodConfig.Image(
                name='some-image',
                version='0.0.2',
            ),
        ],
        mounts=[
            ctr_models.PodConfig.Mount(
                source='',
                target='/tmp',
                read_only=False,
            ),
            ctr_models.PodConfig.Mount(
                source='/path/to/ops/dir/volumes/some-volume',
                target='/some/where',
            ),
        ],
    )

    DEPLOY_INSTRUCTION = models.PodDeployInstruction(
        label='//foo/bar:dummy',
        pod_config_template=POD_CONFIG_TEMPLATE,
        volumes=[
            models.PodDeployInstruction.Volume(
                label='//foo/bar:some-volume',
                version='0.0.3',
                target='/some/where',
            ),
        ],
    )

    def test_make_pod_config(self):
        self.assertEqual(
            pod_ops_dirs.PodOpsDir._make_pod_config(
                self.DEPLOY_INSTRUCTION,
                Path('/path/to/ops/dir'),
                {'message': 'hello world'},
            ),
            self.POD_CONFIG,
        )
예제 #9
0
class PodOpsDirTest(fixtures.TestCaseBase):

    POD_ID = '00000000-0000-0000-0000-000000000001'
    UNIT_1 = models.PodDeployInstruction.SystemdUnitGroup.Unit(
        name='foo.service',
        content='',
    )
    UNIT_2 = models.PodDeployInstruction.SystemdUnitGroup.Unit(
        name='bar.service',
        content='',
        auto_start=False,
    )
    GROUP = models.PodDeployInstruction.SystemdUnitGroup(
        units=[UNIT_1, UNIT_2],
    )
    CONFIG_1 = models.PodMetadata.SystemdUnitConfig(
        pod_id=POD_ID,
        name='foo.service',
    )
    CONFIG_2 = models.PodMetadata.SystemdUnitConfig(
        pod_id=POD_ID,
        name='bar.service',
        auto_start=False,
    )

    DEPLOY_INSTRUCTION = models.PodDeployInstruction(
        label='//foo/bar:dummy',
        pod_config_template=ctr_models.PodConfig(
            name='dummy',
            version='0.0.1',
            apps=[
                ctr_models.PodConfig.App(
                    name='foo',
                    exec=['echo', 'hello', 'world'],
                ),
            ],
            images=[
                ctr_models.PodConfig.Image(
                    name='some-image',
                    version='0.0.2',
                ),
            ],
            mounts=[
                ctr_models.PodConfig.Mount(
                    source='',
                    target='/tmp',
                    read_only=False,
                ),
            ],
        ),
        volumes=[
            models.PodDeployInstruction.Volume(
                label='//foo/bar:some-volume',
                version='0.0.3',
                target='/some/where',
            ),
        ],
        systemd_unit_groups=[
            GROUP,
        ],
        token_names={
            'port_1': 'ops_free_port',
            'port_2': 'ops_free_port',
        },
    )

    BUNDLE_IMAGE_RELPATH = (
        Path(models.POD_BUNDLE_IMAGES_DIR_NAME) / \
        'some-image' /
        models.POD_BUNDLE_IMAGE_FILENAME
    )

    BUNDLE_VOLUME_RELPATH = (
        Path(models.POD_BUNDLE_VOLUMES_DIR_NAME) / \
        'some-volume' /
        models.POD_BUNDLE_VOLUME_FILENAME
    )

    def setUp(self):
        super().setUp()
        self.ctr_scripts_mock = unittest.mock.patch(
            pod_ops_dirs.__name__ + '.ctr_scripts'
        ).start()
        self.scripts_mock = unittest.mock.patch(
            pod_ops_dirs.__name__ + '.scripts'
        ).start()
        self.systemds_mock = unittest.mock.patch(
            pod_ops_dirs.__name__ + '.systemds'
        ).start()
        mock = unittest.mock.patch(
            pod_ops_dirs.__name__ + '.ctr_models.generate_pod_id'
        ).start()
        # It should only be called once.
        mock.side_effect = [self.POD_ID]
        envs.init()
        envs.save({'ops_database_url': 'tcp://127.0.0.1:2390'})
        tokens.init()
        with tokens.make_tokens_database().writing() as active_tokens:
            active_tokens.add_definition(
                'ops_free_port',
                tokens.Tokens.Definition(kind='values', args=['8001', '8002']),
            )

    def tearDown(self):
        unittest.mock.patch.stopall()
        super().tearDown()

    def make_bundle_dir(self):
        jsons.dump_dataobject(
            self.DEPLOY_INSTRUCTION,
            self.test_bundle_dir_path / \
            models.BUNDLE_DEPLOY_INSTRUCTION_FILENAME,
        )
        for path in (
            self.test_bundle_dir_path / self.BUNDLE_IMAGE_RELPATH,
            self.test_bundle_dir_path / self.BUNDLE_VOLUME_RELPATH,
        ):
            path.parent.mkdir(parents=True)
            path.touch()
        return pod_ops_dirs.PodBundleDir(self.test_bundle_dir_path)

    def make_ops_dir(self):
        return pod_ops_dirs.PodOpsDir(self.test_ops_dir_path)

    def test_check_invariants(self):
        bundle_dir = self.make_bundle_dir()
        ops_dir = self.make_ops_dir()
        ops_dir.install(bundle_dir, ops_dir.path)
        with self.assertRaisesRegex(AssertionError, r'expect x.isdisjoint'):
            ops_dir.check_invariants([ops_dir])

    def test_install(self):
        bundle_dir = self.make_bundle_dir()
        ops_dir = self.make_ops_dir()

        # Test install.
        self.assertTrue(ops_dir.install(bundle_dir, ops_dir.path))
        # Check ops dir structure.
        self.assertEqual(
            self.list_dir(self.test_ops_dir_path),
            ['metadata', 'refs', 'volumes'],
        )
        self.assertEqual(
            self.list_dir(self.test_ops_dir_path / 'volumes'),
            ['some-volume'],
        )
        # Check metadata.
        metadata = jsons.load_dataobject(
            models.PodMetadata,
            self.test_ops_dir_path / 'metadata',
        )
        self.assertEqual(metadata.label, self.DEPLOY_INSTRUCTION.label)
        self.assertEqual(metadata.version, self.DEPLOY_INSTRUCTION.version)
        self.assertEqual(metadata.images, self.DEPLOY_INSTRUCTION.images)
        self.assertEqual(
            metadata.systemd_unit_configs,
            [self.CONFIG_1, self.CONFIG_2],
        )
        # Check volumes.
        self.scripts_mock.tar_extract.assert_called_once()
        # Check images.
        self.ctr_scripts_mock.ctr_import_image.assert_called_once_with(
            self.test_bundle_dir_path / self.BUNDLE_IMAGE_RELPATH,
        )
        # Check pods.
        self.ctr_scripts_mock.ctr_prepare_pod.assert_called_once_with(
            self.POD_ID, unittest.mock.ANY
        )
        self.ctr_scripts_mock.ctr_add_ref_to_pod.assert_called_once_with(
            self.POD_ID,
            self.test_ops_dir_path / 'refs' / self.POD_ID,
        )
        # Check systemd units.
        self.systemds_mock.install.assert_has_calls([
            unittest.mock.call(
                self.CONFIG_1,
                ops_dir.metadata,
                self.GROUP,
                self.UNIT_1,
                {
                    'ops_database_url': 'tcp://127.0.0.1:2390',
                },
                {
                    'port_1': '8001',
                    'port_2': '8002',
                },
            ),
            unittest.mock.call(
                self.CONFIG_2,
                ops_dir.metadata,
                self.GROUP,
                self.UNIT_2,
                {
                    'ops_database_url': 'tcp://127.0.0.1:2390',
                },
                {
                    'port_1': '8001',
                    'port_2': '8002',
                },
            ),
        ])
        self.systemds_mock.daemon_reload.assert_called_once()

        # Test uninstall.
        self.systemds_mock.daemon_reload.reset_mock()
        self.assertTrue(ops_dir.uninstall())
        self.systemds_mock.uninstall.assert_has_calls([
            unittest.mock.call(self.CONFIG_1),
            unittest.mock.call(self.CONFIG_2),
        ])
        self.systemds_mock.daemon_reload.assert_called_once()
        self.ctr_scripts_mock.ctr_remove_pod.assert_called_once_with(
            self.POD_ID
        )
        self.ctr_scripts_mock.ctr_remove_image.assert_called_once_with(
            self.DEPLOY_INSTRUCTION.images[0],
            skip_active=True,
        )
        self.assertTrue(g1.files.is_empty_dir(self.test_ops_dir_path))

        self.assertFalse(ops_dir.uninstall())

    def test_start_invalid_args(self):
        ops_dir = self.make_ops_dir()
        with self.assertRaisesRegex(AssertionError, r'expect not all'):
            ops_dir.start(unit_names=[], all_units=True)
        with self.assertRaisesRegex(AssertionError, r'expect not all'):
            ops_dir.start(unit_names=[self.UNIT_1.name], all_units=True)

    def test_start_default(self):
        bundle_dir = self.make_bundle_dir()
        ops_dir = self.make_ops_dir()
        ops_dir.install(bundle_dir, ops_dir.path)
        ops_dir.start()
        self.systemds_mock.activate.assert_called_once_with(self.CONFIG_1)
        self.systemds_mock.deactivate.assert_not_called()

    def test_start_all(self):
        bundle_dir = self.make_bundle_dir()
        ops_dir = self.make_ops_dir()
        ops_dir.install(bundle_dir, ops_dir.path)
        ops_dir.start(all_units=True)
        self.systemds_mock.activate.assert_has_calls([
            unittest.mock.call(self.CONFIG_1),
            unittest.mock.call(self.CONFIG_2),
        ])
        self.systemds_mock.deactivate.assert_not_called()

    def test_start_unit_names(self):
        bundle_dir = self.make_bundle_dir()
        ops_dir = self.make_ops_dir()
        ops_dir.install(bundle_dir, ops_dir.path)
        ops_dir.start(unit_names=['bar.service'])
        self.systemds_mock.activate.assert_called_once_with(self.CONFIG_2)
        self.systemds_mock.deactivate.assert_not_called()

    def test_restart(self):
        bundle_dir = self.make_bundle_dir()
        ops_dir = self.make_ops_dir()
        ops_dir.install(bundle_dir, ops_dir.path)
        ops_dir.restart()
        self.systemds_mock.activate.assert_not_called()
        self.systemds_mock.deactivate.assert_not_called()
        self.systemds_mock.restart.assert_called_once_with(self.CONFIG_1)

    def test_stop(self):
        bundle_dir = self.make_bundle_dir()
        ops_dir = self.make_ops_dir()
        ops_dir.install(bundle_dir, ops_dir.path)
        ops_dir.stop()
        self.systemds_mock.activate.assert_not_called()
        self.systemds_mock.deactivate.assert_has_calls([
            unittest.mock.call(self.CONFIG_1),
            unittest.mock.call(self.CONFIG_2),
        ])
예제 #10
0
class PodsTest(
    fixtures.TestCaseBase,
    filelocks.Fixture if filelocks else object,
):

    sample_pod_id = '01234567-89ab-cdef-0123-456789abcdef'

    sample_config = models.PodConfig(
        name='test-pod',
        version='0.0.1',
        apps=[
            models.PodConfig.App(
                name='hello',
                exec=['/bin/echo', 'hello', 'world'],
            ),
        ],
        images=[
            models.PodConfig.Image(
                name='base',
                version='0.0.1',
            ),
            models.PodConfig.Image(
                name='sample-app',
                version='1.0',
            ),
        ],
        mounts=[
            models.PodConfig.Mount(
                source='/dev/null',
                target='/this/is/pod/path',
                read_only=True,
            ),
        ],
        overlays=[
            models.PodConfig.Overlay(
                sources=[''],
                target='/this/is/some/other/pod/path',
                read_only=False,
            ),
        ],
    )

    sample_image_id = '0123456789abcdef' * 4

    sample_metadata = images.ImageMetadata(
        name='sample-app',
        version='1.0',
    )

    def setUp(self):
        super().setUp()
        bases.cmd_init()
        images.cmd_init()
        pods.cmd_init()
        self.sample_pod_dir_path = pods._get_pod_dir_path(self.sample_pod_id)
        patcher = unittest.mock.patch.object(pods, 'journals')
        self.mock_journals = patcher.start()
        self.addCleanup(patcher.stop)

    @staticmethod
    def make_pod_id(id_int):
        return str(uuid.UUID(int=id_int))

    @staticmethod
    def create_pod_dir(pod_id, config):
        pod_dir_path = pods._get_pod_dir_path(pod_id)
        pod_dir_path.mkdir()
        pods._setup_pod_dir_barely(pod_dir_path, config)
        pods._pod_dir_create_config(pod_dir_path, config)

    @staticmethod
    def list_pod_dir_paths():
        return sorted(p.name for p in pods._iter_pod_dir_paths())

    @staticmethod
    def list_active():
        return sorted(p.name for p in pods._get_active_path().iterdir())

    @staticmethod
    def list_graveyard():
        return sorted(p.name for p in pods._get_graveyard_path().iterdir())

    @staticmethod
    def list_tmp():
        return sorted(p.name for p in pods._get_tmp_path().iterdir())

    @staticmethod
    def make_image_id(id_int):
        return '%064d' % id_int

    @staticmethod
    def create_image_dir(image_id, metadata):
        image_dir_path = images.get_image_dir_path(image_id)
        image_dir_path.mkdir()
        jsons.dump_dataobject(
            metadata,
            images._get_metadata_path(image_dir_path),
        )
        images.get_rootfs_path(image_dir_path).mkdir()

    #
    # Top-level commands.
    #

    def test_cmd_init(self):
        self.assertEqual(
            sorted(p.name for p in pods._get_pod_repo_path().iterdir()),
            ['active', 'graveyard', 'tmp'],
        )

    def test_cmd_list(self):

        def cmd_list():
            return sorted(result['id'] for result in pods.cmd_list())

        self.assertEqual(cmd_list(), [])

        self.create_pod_dir(self.sample_pod_id, self.sample_config)
        for i, image in enumerate(self.sample_config.images):
            self.create_image_dir(
                self.make_image_id(i + 1),
                images.ImageMetadata(name=image.name, version=image.version),
            )
        self.assertEqual(cmd_list(), [self.sample_pod_id])

    def test_cmd_show(self):
        with self.assertRaisesRegex(AssertionError, r'expect.*is_dir'):
            pods.cmd_show(self.sample_pod_id)

        self.create_pod_dir(self.sample_pod_id, self.sample_config)
        self.assertEqual(
            pods.cmd_show(self.sample_pod_id),
            [{
                'name': 'hello',
                'status': None,
                'last-updated': None,
                'ref-count': 1,
            }],
        )

    def test_cmd_cat_config(self):
        with self.assertRaisesRegex(AssertionError, r'expect.*is_file'):
            pods.cmd_cat_config(self.sample_pod_id, io.BytesIO())

        self.create_pod_dir(self.sample_pod_id, self.sample_config)
        buffer = io.BytesIO()
        pods.cmd_cat_config(self.sample_pod_id, buffer)
        self.assertEqual(
            buffer.getvalue(),
            pods._get_config_path(self.sample_pod_dir_path).read_bytes(),
        )

    @unittest.skipUnless(filelocks, 'g1.tests.filelocks unavailable')
    def test_cmd_prepare(self):
        config_path = self.test_repo_path / 'sample-config'
        jsons.dump_dataobject(self.sample_config, config_path)
        for i, image in enumerate(self.sample_config.images):
            self.create_image_dir(
                self.make_image_id(i + 1),
                images.ImageMetadata(name=image.name, version=image.version),
            )
        self.assertEqual(self.list_pod_dir_paths(), [])
        self.assertEqual(list(pods._get_tmp_path().iterdir()), [])

        with unittest.mock.patch.multiple(
            pods.__name__,
            scripts=unittest.mock.DEFAULT,
            # We don't have a valid base image, and so we can't really
            # call ``builders.generate_unit_file``, etc.
            builders=unittest.mock.DEFAULT,
            _generate_hostname=unittest.mock.DEFAULT,
        ):
            pods.cmd_prepare(self.sample_pod_id, config_path)
        self.assertEqual(self.list_pod_dir_paths(), [self.sample_pod_id])
        self.assertEqual(list(pods._get_tmp_path().iterdir()), [])

        self.assertFalse(self.check_exclusive(self.sample_pod_dir_path))

    def test_cmd_remove(self):
        config_path = self.test_repo_path / 'sample-config'
        jsons.dump_dataobject(self.sample_config, config_path)
        self.assertEqual(self.list_pod_dir_paths(), [])
        self.assertEqual(list(pods._get_graveyard_path().iterdir()), [])
        self.assertEqual(list(pods._get_tmp_path().iterdir()), [])

        self.create_pod_dir(self.sample_pod_id, self.sample_config)
        self.assertEqual(pods._get_ref_count(self.sample_pod_dir_path), 1)
        self.assertEqual(self.list_pod_dir_paths(), [self.sample_pod_id])
        self.assertEqual(list(pods._get_graveyard_path().iterdir()), [])
        self.assertEqual(list(pods._get_tmp_path().iterdir()), [])

        ref_path = self.test_repo_path / 'ref'
        pods.cmd_add_ref(self.sample_pod_id, ref_path)
        with unittest.mock.patch(pods.__name__ + '.scripts'):
            with self.assertRaisesRegex(
                AssertionError, r'expect x <= 1, not 2'
            ):
                pods.cmd_remove(self.sample_pod_id)
        self.assertEqual(pods._get_ref_count(self.sample_pod_dir_path), 2)
        self.assertEqual(self.list_pod_dir_paths(), [self.sample_pod_id])
        self.assertEqual(list(pods._get_graveyard_path().iterdir()), [])
        self.assertEqual(list(pods._get_tmp_path().iterdir()), [])
        self.mock_journals.remove_journal_dir.assert_not_called()

        ref_path.unlink()
        with unittest.mock.patch(pods.__name__ + '.scripts'):
            pods.cmd_remove(self.sample_pod_id)
        self.assertEqual(self.list_pod_dir_paths(), [])
        self.assertEqual(list(pods._get_graveyard_path().iterdir()), [])
        self.assertEqual(list(pods._get_tmp_path().iterdir()), [])
        self.mock_journals.remove_journal_dir.assert_called_once_with(
            self.sample_pod_id
        )

    @unittest.skipUnless(filelocks, 'g1.tests.filelocks unavailable')
    def test_cmd_cleanup(self):
        future = datetimes.utcnow() + datetime.timedelta(days=1)
        pod_id_1 = self.make_pod_id(1)
        pod_id_2 = self.make_pod_id(2)
        self.create_pod_dir(pod_id_1, self.sample_config)
        self.create_pod_dir(pod_id_2, self.sample_config)
        self.assertEqual(self.list_active(), [pod_id_1, pod_id_2])
        self.assertEqual(self.list_graveyard(), [])
        self.assertEqual(self.list_tmp(), [])

        ref_path = self.test_repo_path / 'ref'
        pods.cmd_add_ref(pod_id_1, ref_path)
        pods.cmd_cleanup(future)
        self.assertEqual(self.list_active(), [pod_id_1])
        self.assertEqual(self.list_graveyard(), [])
        self.assertEqual(self.list_tmp(), [])
        ref_path.unlink()
        self.mock_journals.remove_journal_dir.assert_called_once_with(pod_id_2)

        self.mock_journals.remove_journal_dir.reset_mock()
        with self.using_exclusive(pods._get_pod_dir_path(pod_id_1)):
            pods.cmd_cleanup(future)
        self.assertEqual(self.list_active(), [pod_id_1])
        self.assertEqual(self.list_graveyard(), [])
        self.assertEqual(self.list_tmp(), [])
        self.mock_journals.remove_journal_dir.assert_not_called()

        self.mock_journals.remove_journal_dir.reset_mock()
        pods.cmd_cleanup(future)
        self.assertEqual(self.list_active(), [])
        self.assertEqual(self.list_graveyard(), [])
        self.assertEqual(self.list_tmp(), [])
        self.mock_journals.remove_journal_dir.assert_called_once_with(pod_id_1)

    @unittest.skipUnless(filelocks, 'g1.tests.filelocks unavailable')
    def test_cleanup_active(self):
        future = datetimes.utcnow() + datetime.timedelta(days=1)
        pod_id_1 = self.make_pod_id(1)
        pod_id_2 = self.make_pod_id(2)
        self.create_pod_dir(pod_id_1, self.sample_config)
        self.create_pod_dir(pod_id_2, self.sample_config)
        self.assertEqual(self.list_active(), [pod_id_1, pod_id_2])
        self.assertEqual(self.list_graveyard(), [])
        self.assertEqual(self.list_tmp(), [])

        with self.using_exclusive(pods._get_pod_dir_path(pod_id_1)):
            pods._cleanup_active(future)
        self.assertEqual(self.list_active(), [pod_id_1])
        self.assertEqual(self.list_graveyard(), [pod_id_2])
        self.assertEqual(self.list_tmp(), [])
        self.mock_journals.remove_journal_dir.assert_called_once_with(pod_id_2)

        self.mock_journals.remove_journal_dir.reset_mock()
        pods._cleanup_active(future)
        self.assertEqual(self.list_active(), [])
        self.assertEqual(self.list_graveyard(), [pod_id_1, pod_id_2])
        self.assertEqual(self.list_tmp(), [])
        self.mock_journals.remove_journal_dir.assert_called_once_with(pod_id_1)

    #
    # Locking strategy.
    #

    @unittest.skipUnless(filelocks, 'g1.tests.filelocks unavailable')
    def test_create_tmp_pod_dir(self):
        tmp_path = pods._create_tmp_pod_dir()
        self.assertFalse(self.check_exclusive(tmp_path))

    #
    # Data type.
    #

    def test_config(self):
        with self.assertRaisesRegex(AssertionError, r'expect non-empty'):
            models.PodConfig(
                name='test-pod',
                version='0.0.1',
                apps=self.sample_config.apps,
                images=[],
            )
        with self.assertRaisesRegex(
            AssertionError, r'expect unique elements in '
        ):
            models.PodConfig(
                name='test-pod',
                version='0.0.1',
                apps=[
                    models.PodConfig.App(name='some-app', exec=['/bin/true']),
                    models.PodConfig.App(name='some-app', exec=['/bin/false']),
                ],
                images=self.sample_config.images,
            )
        with self.assertRaisesRegex(
            AssertionError, r'expect unique elements in '
        ):
            models.PodConfig(
                name='test-pod',
                version='0.0.1',
                apps=self.sample_config.apps,
                images=self.sample_config.images,
                mounts=[
                    models.PodConfig.Mount(source='/p', target='/a'),
                ],
                overlays=[
                    models.PodConfig.Overlay(sources=['/q'], target='/a'),
                ],
            )
        with self.assertRaisesRegex(AssertionError, r'expect only one'):
            models.PodConfig.Image()
        with self.assertRaisesRegex(AssertionError, r'expect.*xor.*be false'):
            models.PodConfig.Image(name='name')
        with self.assertRaisesRegex(AssertionError, r'expect.*is_absolute'):
            models.PodConfig.Mount(source='foo', target='/bar')
        with self.assertRaisesRegex(AssertionError, r'expect.*is_absolute'):
            models.PodConfig.Mount(source='/foo', target='bar')
        with self.assertRaisesRegex(AssertionError, r'expect non-empty'):
            models.PodConfig.Overlay(sources=[], target='/bar')
        with self.assertRaisesRegex(AssertionError, r'expect.*is_absolute'):
            models.PodConfig.Overlay(sources=['foo'], target='/bar')
        with self.assertRaisesRegex(AssertionError, r'expect.*is_absolute'):
            models.PodConfig.Overlay(sources=['/foo'], target='bar')
        with self.assertRaisesRegex(AssertionError, r'expect x == 1, not 0'):
            models.PodConfig.Overlay(sources=['', '/foo'], target='/bar')

    def test_validate_id(self):
        self.assertEqual(
            models.validate_pod_id(self.sample_pod_id), self.sample_pod_id
        )
        for test_data in (
            '',
            '01234567-89AB-CDEF-0123-456789ABCDEF',
            '01234567-89ab-cdef-0123-456789abcde',
        ):
            with self.subTest(test_data):
                with self.assertRaisesRegex(
                    AssertionError, r'expect .*fullmatch.*'
                ):
                    models.validate_pod_id(test_data)

    def test_id_converter(self):
        self.assertEqual(
            models.
            pod_id_to_machine_id('01234567-89ab-cdef-0123-456789abcdef'),
            '0123456789abcdef0123456789abcdef',
        )
        self.assertEqual(
            models.machine_id_to_pod_id('0123456789abcdef0123456789abcdef'),
            '01234567-89ab-cdef-0123-456789abcdef',
        )

    def test_generate_id(self):
        id1 = models.generate_pod_id()
        id2 = models.generate_pod_id()
        self.assertNotEqual(id1, id2)
        self.assertEqual(models.validate_pod_id(id1), id1)
        self.assertEqual(models.validate_pod_id(id2), id2)

    #
    # Repo layout.
    #

    def test_repo_layout(self):
        for path1, path2 in (
            (
                pods._get_pod_repo_path(),
                bases.get_repo_path() / 'pods',
            ),
            (
                pods._get_active_path(),
                pods._get_pod_repo_path() / 'active',
            ),
            (
                pods._get_graveyard_path(),
                pods._get_pod_repo_path() / 'graveyard',
            ),
            (
                pods._get_tmp_path(),
                pods._get_pod_repo_path() / 'tmp',
            ),
            (
                pods._get_pod_dir_path(self.sample_pod_id),
                pods._get_active_path() / self.sample_pod_id,
            ),
            (
                pods._get_id(self.sample_pod_dir_path),
                self.sample_pod_id,
            ),
            (
                pods._get_config_path(self.sample_pod_dir_path),
                pods._get_active_path() / self.sample_pod_id / 'config',
            ),
            (
                pods._get_orig_config_path(self.sample_pod_dir_path),
                pods._get_active_path() / self.sample_pod_id / 'config.orig',
            ),
            (
                pods._get_deps_path(self.sample_pod_dir_path),
                pods._get_active_path() / self.sample_pod_id / 'deps',
            ),
            (
                pods._get_work_path(self.sample_pod_dir_path),
                pods._get_active_path() / self.sample_pod_id / 'work',
            ),
            (
                pods._get_upper_path(self.sample_pod_dir_path),
                pods._get_active_path() / self.sample_pod_id / 'upper',
            ),
            (
                pods._get_rootfs_path(self.sample_pod_dir_path),
                pods._get_active_path() / self.sample_pod_id / 'rootfs',
            ),
        ):
            with self.subTest((path1, path2)):
                self.assertEqual(path1, path2)

    #
    # Top-level directories.
    #

    def test_cleanup_top_dir(self):
        pod_id_1 = self.make_pod_id(1)
        pod_id_2 = self.make_pod_id(2)
        self.create_pod_dir(pod_id_1, self.sample_config)
        self.create_pod_dir(pod_id_2, self.sample_config)
        self.assertEqual(self.list_pod_dir_paths(), [pod_id_1, pod_id_2])

        with unittest.mock.patch(pods.__name__ + '.scripts'):

            with locks.acquiring_exclusive(pods._get_pod_dir_path(pod_id_2)):
                pods._cleanup_top_dir(pods._get_active_path())
            self.assertEqual(self.list_pod_dir_paths(), [pod_id_2])

            pods._cleanup_top_dir(pods._get_active_path())
            self.assertEqual(self.list_pod_dir_paths(), [])

    #
    # Pod directories.
    #

    def test_iter_pod_dir_paths(self):
        pod_id_1 = self.make_pod_id(1)
        pod_id_2 = self.make_pod_id(2)

        (pods._get_active_path() / 'irrelevant').touch()
        self.assertEqual(self.list_pod_dir_paths(), [])
        self.assertEqual(self.list_active(), ['irrelevant'])

        self.create_pod_dir(pod_id_2, self.sample_config)
        self.assertEqual(self.list_pod_dir_paths(), [pod_id_2])

        (pods._get_active_path() / pod_id_1).mkdir()
        self.assertEqual(self.list_pod_dir_paths(), [pod_id_1, pod_id_2])

    def test_maybe_move_pod_dir_to_active(self):
        self.assertEqual(self.list_pod_dir_paths(), [])

        path = self.test_repo_path / 'some-dir'
        path.mkdir()
        self.assertTrue(path.exists())
        self.assertTrue(
            pods._maybe_move_pod_dir_to_active(path, self.sample_pod_id)
        )
        self.assertEqual(self.list_pod_dir_paths(), [self.sample_pod_id])
        self.assertFalse(path.exists())

        path.mkdir()
        self.assertFalse(
            pods._maybe_move_pod_dir_to_active(path, self.sample_pod_id)
        )

    def test_move_pod_dir_to_graveyard(self):

        def list_grave_paths():
            return sorted(p.name for p in pods._get_graveyard_path().iterdir())

        self.assertEqual(list_grave_paths(), [])

        self.create_pod_dir(self.sample_pod_id, self.sample_config)
        self.assertTrue(self.sample_pod_dir_path.exists())
        pods._move_pod_dir_to_graveyard(self.sample_pod_dir_path)
        self.assertEqual(list_grave_paths(), [self.sample_pod_id])
        self.assertFalse(self.sample_pod_dir_path.exists())

    #
    # Pod directory.
    #

    def test_prepare_pod_dir(self):
        self.sample_pod_dir_path.mkdir()
        for i, image in enumerate(self.sample_config.images):
            self.create_image_dir(
                self.make_image_id(i + 1),
                images.ImageMetadata(name=image.name, version=image.version),
            )
        with unittest.mock.patch.multiple(
            pods.__name__,
            scripts=unittest.mock.DEFAULT,
            # We don't have a valid base image, and so we can't really
            # call ``builders.generate_unit_file``, etc.
            builders=unittest.mock.DEFAULT,
            _generate_hostname=unittest.mock.DEFAULT,
        ):
            pods._prepare_pod_dir(
                self.sample_pod_dir_path,
                self.sample_pod_id,
                self.sample_config,
            )

    def test_setup_pod_dir_barely(self):
        pod_dir_path = pods._get_pod_dir_path(self.sample_pod_id)
        pod_dir_path.mkdir()
        pods._setup_pod_dir_barely(pod_dir_path, self.sample_config)
        self.assertFalse(
            pods._get_config_path(self.sample_pod_dir_path).is_file()
        )
        self.assertTrue(
            pods._get_orig_config_path(self.sample_pod_dir_path).is_file()
        )
        self.assertTrue(pods._get_deps_path(self.sample_pod_dir_path).is_dir())
        self.assertTrue(pods._get_work_path(self.sample_pod_dir_path).is_dir())
        self.assertTrue(
            pods._get_upper_path(self.sample_pod_dir_path).is_dir()
        )
        self.assertTrue(
            pods._get_rootfs_path(self.sample_pod_dir_path).is_dir()
        )
        self.assertEqual(
            sorted(p.name for p in self.sample_pod_dir_path.iterdir()),
            ['config.orig', 'deps', 'rootfs', 'upper', 'work'],
        )

    def test_remove_pod_dir(self):
        self.create_pod_dir(self.sample_pod_id, self.sample_config)
        self.assertTrue(self.sample_pod_dir_path.is_dir())
        with unittest.mock.patch(pods.__name__ + '.scripts'):
            pods._remove_pod_dir(self.sample_pod_dir_path)
        self.assertFalse(self.sample_pod_dir_path.exists())

    #
    # Pod.
    #

    @unittest.mock.patch(pods.__name__ + '.scripts')
    def test_mount_overlay(self, scripts_mock):
        image_id_1 = self.make_image_id(1)
        image_id_2 = self.make_image_id(2)
        self.create_image_dir(
            image_id_1,
            images.ImageMetadata(name='base', version='0.0.1'),
        )
        self.create_image_dir(image_id_2, self.sample_metadata)

        pods._mount_overlay(self.sample_pod_dir_path, self.sample_config)
        scripts_mock.run.assert_called_once_with([
            'mount',
            *('-t', 'overlay'),
            *(
                '-o',
                'lowerdir=%s,upperdir=%s,workdir=%s' % (
                    ':'.join([
                        str(pods._get_image_rootfs_path(image_id_2)),
                        str(pods._get_image_rootfs_path(image_id_1)),
                    ]),
                    pods._get_upper_path(self.sample_pod_dir_path),
                    pods._get_work_path(self.sample_pod_dir_path),
                ),
            ),
            'overlay',
            pods._get_rootfs_path(self.sample_pod_dir_path),
        ])

    def test_make_bind_argument(self):
        self.assertEqual(
            pods._make_bind_argument(
                models.PodConfig.Mount(
                    source='/a',
                    target='/b',
                    read_only=True,
                )
            ),
            '--bind-ro=/a:/b',
        )
        self.assertEqual(
            pods._make_bind_argument(
                models.PodConfig.Mount(
                    source='/a',
                    target='/b',
                    read_only=False,
                )
            ),
            '--bind=/a:/b',
        )

    def test_make_overlay_argument(self):
        self.assertEqual(
            pods._make_overlay_argument(
                models.PodConfig.Overlay(
                    sources=['/a', '/b'],
                    target='/c',
                    read_only=True,
                )
            ),
            '--overlay-ro=/a:/b:/c',
        )
        self.assertEqual(
            pods._make_overlay_argument(
                models.PodConfig.Overlay(
                    sources=['/a', ''],
                    target='/b',
                    read_only=False,
                )
            ),
            '--overlay=/a::/b',
        )

    #
    # Configs.
    #

    def test_iter_configs(self):

        def list_configs():
            return sorted((p.name, c) for p, c in pods._iter_configs())

        pod_id_1 = self.make_pod_id(1)
        pod_id_2 = self.make_pod_id(2)

        self.assertEqual(list_configs(), [])

        self.create_pod_dir(pod_id_2, self.sample_config)
        self.assertEqual(list_configs(), [(pod_id_2, self.sample_config)])

        self.create_pod_dir(pod_id_1, self.sample_config)
        self.assertEqual(
            list_configs(),
            [(pod_id_1, self.sample_config), (pod_id_2, self.sample_config)],
        )

    def test_read_config(self):
        self.create_pod_dir(self.sample_pod_id, self.sample_config)
        self.assertEqual(
            pods._read_config(self.sample_pod_dir_path),
            self.sample_config,
        )

    def test_write_config(self):
        self.assertFalse((self.test_repo_path / 'config').exists())
        pods._write_config(self.sample_config, self.test_repo_path)
        self.assertTrue((self.test_repo_path / 'config').exists())
        self.assertEqual(
            pods._read_config(self.test_repo_path),
            self.sample_config,
        )
        self.assertFalse((self.test_repo_path / 'config.orig').exists())

    def test_write_orig_config(self):
        self.assertFalse((self.test_repo_path / 'config.orig').exists())
        pods._write_orig_config(self.sample_config, self.test_repo_path)
        self.assertTrue((self.test_repo_path / 'config.orig').exists())
        self.assertEqual(
            pods._read_orig_config(self.test_repo_path),
            self.sample_config,
        )
        self.assertFalse((self.test_repo_path / 'config').exists())

    def test_iter_image_ids(self):

        def list_image_ids(config):
            return sorted(pods._iter_image_ids(config))

        self.create_image_dir(self.sample_image_id, self.sample_metadata)
        images.cmd_tag(image_id=self.sample_image_id, new_tag='some-tag')

        config = models.PodConfig(
            name='test-pod',
            version='0.0.1',
            apps=self.sample_config.apps,
            images=[models.PodConfig.Image(id=self.sample_image_id)],
        )
        self.assertEqual(list_image_ids(config), [self.sample_image_id])

        config = models.PodConfig(
            name='test-pod',
            version='0.0.1',
            apps=self.sample_config.apps,
            images=[models.PodConfig.Image(name='sample-app', version='1.0')],
        )
        self.assertEqual(list_image_ids(config), [self.sample_image_id])

        config = models.PodConfig(
            name='test-pod',
            version='0.0.1',
            apps=self.sample_config.apps,
            images=[models.PodConfig.Image(tag='some-tag')],
        )
        self.assertEqual(list_image_ids(config), [self.sample_image_id])

        config = models.PodConfig(
            name='test-pod',
            version='0.0.1',
            apps=self.sample_config.apps,
            images=[models.PodConfig.Image(name='no-such-app', version='1.0')],
        )
        with self.assertRaisesRegex(AssertionError, r'expect non-None value'):
            list_image_ids(config)

        config = models.PodConfig(
            name='test-pod',
            version='0.0.1',
            apps=self.sample_config.apps,
            images=[models.PodConfig.Image(tag='no-such-tag')],
        )
        with self.assertRaisesRegex(AssertionError, r'expect non-None value'):
            list_image_ids(config)

    #
    # Dependent images.
    #

    def test_add_ref_image_ids(self):

        def list_image_ids():
            return sorted(
                p.name for p in \
                pods._get_deps_path(self.sample_pod_dir_path).iterdir()
            )

        image_id_1 = self.make_image_id(1)
        image_id_2 = self.make_image_id(2)
        self.create_pod_dir(self.sample_pod_id, self.sample_config)
        self.create_image_dir(image_id_1, self.sample_metadata)
        self.create_image_dir(image_id_2, self.sample_metadata)
        self.assertEqual(list_image_ids(), [])
        self.assertEqual(
            images._get_ref_count(images.get_image_dir_path(image_id_1)), 1
        )
        self.assertEqual(
            images._get_ref_count(images.get_image_dir_path(image_id_2)), 1
        )

        config = models.PodConfig(
            name='test-pod',
            version='0.0.1',
            apps=self.sample_config.apps,
            images=[
                models.PodConfig.Image(id=image_id_1),
                models.PodConfig.Image(id=image_id_2),
            ],
        )

        new_config = pods._add_ref_image_ids(self.sample_pod_dir_path, config)
        self.assertEqual(config, new_config)
        self.assertEqual(list_image_ids(), [image_id_1, image_id_2])
        self.assertEqual(
            images._get_ref_count(images.get_image_dir_path(image_id_1)), 2
        )
        self.assertEqual(
            images._get_ref_count(images.get_image_dir_path(image_id_2)), 2
        )

    def test_iter_ref_image_ids(self):

        def list_ref_image_ids(pod_dir_path):
            return sorted(pods._iter_ref_image_ids(pod_dir_path))

        image_id_1 = self.make_image_id(1)
        image_id_2 = self.make_image_id(2)
        self.create_pod_dir(self.sample_pod_id, self.sample_config)
        self.create_image_dir(image_id_1, self.sample_metadata)
        self.create_image_dir(image_id_2, self.sample_metadata)
        self.assertEqual(list_ref_image_ids(self.sample_pod_dir_path), [])

        pods._add_ref_image_ids(
            self.sample_pod_dir_path,
            models.PodConfig(
                name='test-pod',
                version='0.0.1',
                apps=self.sample_config.apps,
                images=[
                    models.PodConfig.Image(id=image_id_1),
                    models.PodConfig.Image(id=image_id_2),
                ],
            ),
        )
        self.assertEqual(
            list_ref_image_ids(self.sample_pod_dir_path),
            [image_id_1, image_id_2],
        )

    #
    # Pod runtime state.
    #

    def test_get_pod_status(self):
        self.assertEqual(
            pods._get_pod_status(self.sample_pod_dir_path, self.sample_config),
            {},
        )

        self.create_pod_dir(self.sample_pod_id, self.sample_config)
        app = self.sample_config.apps[0]
        path = builders._get_pod_app_exit_status_path(
            pods._get_rootfs_path(self.sample_pod_dir_path), app
        )
        path.parent.mkdir(parents=True)
        path.write_text('99')
        pod_status = pods._get_pod_status(
            self.sample_pod_dir_path, self.sample_config
        )
        self.assertEqual(list(pod_status.keys()), [app.name])
        self.assertEqual(pod_status[app.name][0], 99)

    def test_get_last_updated(self):
        self.assertIsNone(pods._get_last_updated({}))
        self.assertEqual(
            pods._get_last_updated({
                'app-1': (0, datetime.datetime(2001, 1, 1)),
            }),
            datetime.datetime(2001, 1, 1),
        )
        self.assertEqual(
            pods._get_last_updated({
                'app-1': (0, datetime.datetime(2001, 1, 1)),
                'app-2': (0, datetime.datetime(2002, 1, 1)),
            }),
            datetime.datetime(2002, 1, 1),
        )

    #
    # Helpers for mount/umount.
    #

    def test_umount(self):
        path1 = pods._get_pod_repo_path() / 'some-file'
        path2 = pods._get_pod_repo_path() / 'some-dir'
        path3 = pods._get_pod_repo_path() / 'link-to-dir'
        path1.touch()
        path2.mkdir()
        path3.symlink_to(path2)
        pods._umount(path1)
        pods._umount(path2)
        with self.assertRaisesRegex(AssertionError, r'expect not.*is_symlink'):
            pods._umount(path3)