def test_merge_volume_bindings(self): options = [ VolumeSpec.parse('/host/volume:/host/volume:ro'), VolumeSpec.parse('/host/rw/volume:/host/rw/volume'), VolumeSpec.parse('/new/volume'), VolumeSpec.parse('/existing/volume'), ] self.mock_client.inspect_image.return_value = { 'ContainerConfig': {'Volumes': {}} } intermediate_container = Container(self.mock_client, { 'Image': 'ababab', 'Volumes': {'/existing/volume': '/var/lib/docker/aaaaaaaa'}, }, has_been_inspected=True) expected = [ '/host/volume:/host/volume:ro', '/host/rw/volume:/host/rw/volume:rw', '/var/lib/docker/aaaaaaaa:/existing/volume:rw', ] binds = merge_volume_bindings(options, intermediate_container) self.assertEqual(set(binds), set(expected))
def test_get_container_data_volumes(self): options = [VolumeSpec.parse(v) for v in [ '/host/volume:/host/volume:ro', '/new/volume', '/existing/volume', ]] self.mock_client.inspect_image.return_value = { 'ContainerConfig': { 'Volumes': { '/mnt/image/data': {}, } } } container = Container(self.mock_client, { 'Image': 'ababab', 'Volumes': { '/host/volume': '/host/volume', '/existing/volume': '/var/lib/docker/aaaaaaaa', '/removed/volume': '/var/lib/docker/bbbbbbbb', '/mnt/image/data': '/var/lib/docker/cccccccc', }, }, has_been_inspected=True) expected = [ VolumeSpec.parse('/var/lib/docker/aaaaaaaa:/existing/volume:rw'), VolumeSpec.parse('/var/lib/docker/cccccccc:/mnt/image/data:rw'), ] volumes = get_container_data_volumes(container, options) assert sorted(volumes) == sorted(expected)
def test_merge_volume_bindings(self): options = [ VolumeSpec.parse('/host/volume:/host/volume:ro'), VolumeSpec.parse('/host/rw/volume:/host/rw/volume'), VolumeSpec.parse('/new/volume'), VolumeSpec.parse('/existing/volume'), ] self.mock_client.inspect_image.return_value = { 'ContainerConfig': {'Volumes': {}} } previous_container = Container(self.mock_client, { 'Id': 'cdefab', 'Image': 'ababab', 'Mounts': [{ 'Source': '/var/lib/docker/aaaaaaaa', 'Destination': '/existing/volume', 'Mode': '', 'RW': True, 'Name': 'existingvolume', }], }, has_been_inspected=True) expected = [ '/host/volume:/host/volume:ro', '/host/rw/volume:/host/rw/volume:rw', 'existingvolume:/existing/volume:rw', ] binds, affinity = merge_volume_bindings(options, previous_container) assert sorted(binds) == sorted(expected) assert affinity == {'affinity:container': '=cdefab'}
def test_mount_same_host_path_to_two_volumes(self): service = Service( 'web', image='busybox', volumes=[ VolumeSpec.parse('/host/path:/data1'), VolumeSpec.parse('/host/path:/data2'), ], client=self.mock_client, ) self.mock_client.inspect_image.return_value = { 'Id': 'ababab', 'ContainerConfig': { 'Volumes': {} } } service._get_container_create_options( override_options={}, number=1, ) self.assertEqual( set(self.mock_client.create_host_config.call_args[1]['binds']), set([ '/host/path:/data1:rw', '/host/path:/data2:rw', ]), )
def test_get_container_data_volumes(self): options = [VolumeSpec.parse(v) for v in [ '/host/volume:/host/volume:ro', '/new/volume', '/existing/volume', 'named:/named/vol', '/dev/tmpfs' ]] self.mock_client.inspect_image.return_value = { 'ContainerConfig': { 'Volumes': { '/mnt/image/data': {}, } } } container = Container(self.mock_client, { 'Image': 'ababab', 'Mounts': [ { 'Source': '/host/volume', 'Destination': '/host/volume', 'Mode': '', 'RW': True, 'Name': 'hostvolume', }, { 'Source': '/var/lib/docker/aaaaaaaa', 'Destination': '/existing/volume', 'Mode': '', 'RW': True, 'Name': 'existingvolume', }, { 'Source': '/var/lib/docker/bbbbbbbb', 'Destination': '/removed/volume', 'Mode': '', 'RW': True, 'Name': 'removedvolume', }, { 'Source': '/var/lib/docker/cccccccc', 'Destination': '/mnt/image/data', 'Mode': '', 'RW': True, 'Name': 'imagedata', }, ] }, has_been_inspected=True) expected = [ VolumeSpec.parse('existingvolume:/existing/volume:rw'), VolumeSpec.parse('imagedata:/mnt/image/data:rw'), ] volumes = get_container_data_volumes(container, options, ['/dev/tmpfs']) assert sorted(volumes) == sorted(expected)
def test_parse_volume_windows_absolute_path_native(self): windows_path = "c:\\Users\\me\\Documents\\shiny\\config:/opt/shiny/config:ro" assert VolumeSpec._parse_win32(windows_path, False) == ( "c:\\Users\\me\\Documents\\shiny\\config", "/opt/shiny/config", "ro" )
def test_parse_volume_windows_absolute_path_normalized(self): windows_path = "c:\\Users\\me\\Documents\\shiny\\config:/opt/shiny/config:ro" assert VolumeSpec._parse_win32(windows_path, True) == ( "/c/Users/me/Documents/shiny/config", "/opt/shiny/config", "ro" )
def test_parse_volume_windows_just_drives_native(self): windows_path = 'E:\\:C:\\:ro' assert VolumeSpec._parse_win32(windows_path, False) == ( 'E:\\', 'C:\\', 'ro' )
def test_parse_volume_windows_mixed_notations_normalized(self): windows_path = 'C:\\Foo:/root/foo' assert VolumeSpec._parse_win32(windows_path, True) == ( '/c/Foo', '/root/foo', 'rw' )
def test_duplicate_volume_trailing_slash(self): """ When an image specifies a volume, and the Compose file specifies a host path but adds a trailing slash, make sure that we don't create duplicate binds. """ host_path = "/tmp/data" container_path = "/data" volumes = [VolumeSpec.parse("{}:{}/".format(host_path, container_path))] tmp_container = self.client.create_container( "busybox", "true", volumes={container_path: {}}, labels={"com.docker.compose.test_image": "true"} ) image = self.client.commit(tmp_container)["Id"] service = self.create_service("db", image=image, volumes=volumes) old_container = create_and_start_container(service) self.assertEqual(old_container.get("Config.Volumes"), {container_path: {}}) service = self.create_service("db", image=image, volumes=volumes) new_container = service.recreate_container(old_container) self.assertEqual(new_container.get("Config.Volumes"), {container_path: {}}) self.assertEqual(service.containers(stopped=False), [new_container])
def test_execute_convergence_plan_recreate(self): service = self.create_service( "db", environment={"FOO": "1"}, volumes=[VolumeSpec.parse("/etc")], entrypoint=["top"], command=["-d", "1"] ) old_container = service.create_container() self.assertEqual(old_container.get("Config.Entrypoint"), ["top"]) self.assertEqual(old_container.get("Config.Cmd"), ["-d", "1"]) self.assertIn("FOO=1", old_container.get("Config.Env")) self.assertEqual(old_container.name, "composetest_db_1") service.start_container(old_container) old_container.inspect() # reload volume data volume_path = old_container.get_mount("/etc")["Source"] num_containers_before = len(self.client.containers(all=True)) service.options["environment"]["FOO"] = "2" new_container, = service.execute_convergence_plan(ConvergencePlan("recreate", [old_container])) self.assertEqual(new_container.get("Config.Entrypoint"), ["top"]) self.assertEqual(new_container.get("Config.Cmd"), ["-d", "1"]) self.assertIn("FOO=2", new_container.get("Config.Env")) self.assertEqual(new_container.name, "composetest_db_1") self.assertEqual(new_container.get_mount("/etc")["Source"], volume_path) self.assertIn("affinity:container==%s" % old_container.id, new_container.get("Config.Env")) self.assertEqual(len(self.client.containers(all=True)), num_containers_before) self.assertNotEqual(old_container.id, new_container.id) self.assertRaises(APIError, self.client.inspect_container, old_container.id)
def test_parse_volume_windows_just_drives_normalized(self): windows_path = 'E:\\:C:\\:ro' assert VolumeSpec._parse_win32(windows_path, True) == ( '/e/', 'C:\\', 'ro' )
def test_parse_volume_windows_internal_path_normalized(self): windows_path = 'C:\\Users\\reimu\\scarlet:C:\\scarlet\\app:ro' assert VolumeSpec._parse_win32(windows_path, True) == ( '/c/Users/reimu/scarlet', 'C:\\scarlet\\app', 'ro' )
def test_different_host_path_in_container_json(self): service = Service( 'web', image='busybox', volumes=[VolumeSpec.parse('/host/path:/data')], client=self.mock_client, ) self.mock_client.inspect_image.return_value = { 'Id': 'ababab', 'ContainerConfig': { 'Volumes': { '/data': {}, } } } self.mock_client.inspect_container.return_value = { 'Id': '123123123', 'Image': 'ababab', 'Volumes': { '/data': '/mnt/sda1/host/path', }, } service._get_container_create_options( override_options={}, number=1, previous_container=Container(self.mock_client, {'Id': '123123123'}), ) self.assertEqual( self.mock_client.create_host_config.call_args[1]['binds'], ['/mnt/sda1/host/path:/data:rw'], )
def test_duplicate_volume_trailing_slash(self): """ When an image specifies a volume, and the Compose file specifies a host path but adds a trailing slash, make sure that we don't create duplicate binds. """ host_path = '/tmp/data' container_path = '/data' volumes = [VolumeSpec.parse('{}:{}/'.format(host_path, container_path))] tmp_container = self.client.create_container( 'busybox', 'true', volumes={container_path: {}}, labels={'com.docker.compose.test_image': 'true'}, ) image = self.client.commit(tmp_container)['Id'] service = self.create_service('db', image=image, volumes=volumes) old_container = create_and_start_container(service) self.assertEqual( old_container.get('Config.Volumes'), {container_path: {}}, ) service = self.create_service('db', image=image, volumes=volumes) new_container = service.recreate_container(old_container) self.assertEqual( new_container.get('Config.Volumes'), {container_path: {}}, ) self.assertEqual(service.containers(stopped=False), [new_container])
def test_execute_convergence_plan_when_image_volume_masks_config(self): service = self.create_service( 'db', build={'context': 'tests/fixtures/dockerfile-with-volume'}, ) old_container = create_and_start_container(service) self.assertEqual( [mount['Destination'] for mount in old_container.get('Mounts')], ['/data'] ) volume_path = old_container.get_mount('/data')['Source'] service.options['volumes'] = [VolumeSpec.parse('/tmp:/data')] with mock.patch('compose.service.log') as mock_log: new_container, = service.execute_convergence_plan( ConvergencePlan('recreate', [old_container])) mock_log.warn.assert_called_once_with(mock.ANY) _, args, kwargs = mock_log.warn.mock_calls[0] self.assertIn( "Service \"db\" is using volume \"/data\" from the previous container", args[0]) self.assertEqual( [mount['Destination'] for mount in new_container.get('Mounts')], ['/data'] ) self.assertEqual(new_container.get_mount('/data')['Source'], volume_path)
def test_parse_volume_windows_absolute_path(self): windows_path = "c:\\Users\\me\\Documents\\shiny\\config:\\opt\\shiny\\config:ro" assert VolumeSpec.parse(windows_path) == ( "/c/Users/me/Documents/shiny/config", "/opt/shiny/config", "ro" )
def test_project_up_with_no_recreate_stopped(self): web = self.create_service('web') db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')]) project = Project('composetest', [web, db], self.client) project.start() self.assertEqual(len(project.containers()), 0) project.up(['db']) project.kill() old_containers = project.containers(stopped=True) self.assertEqual(len(old_containers), 1) old_container, = old_containers old_db_id = old_container.id db_volume_path = old_container.get_mount('/var/db')['Source'] project.up(strategy=ConvergenceStrategy.never) new_containers = project.containers(stopped=True) self.assertEqual(len(new_containers), 2) self.assertEqual([c.is_running for c in new_containers], [True, True]) db_container = [c for c in new_containers if 'db' in c.name][0] self.assertEqual(db_container.id, old_db_id) self.assertEqual( db_container.get_mount('/var/db')['Source'], db_volume_path)
def test_parse_volume_windows_internal_path_native(self): windows_path = 'C:\\Users\\reimu\\scarlet:C:\\scarlet\\app:ro' assert VolumeSpec._parse_win32(windows_path, False) == ( 'C:\\Users\\reimu\\scarlet', 'C:\\scarlet\\app', 'ro' )
def test_parse_volume_windows_mixed_notations_native(self): windows_path = 'C:\\Foo:/root/foo' assert VolumeSpec._parse_win32(windows_path, False) == ( 'C:\\Foo', '/root/foo', 'rw' )
def setUp(self): self.db = self.create_service( 'db', volumes=[VolumeSpec.parse('/var/db')], command='top') self.project = Project('composetest', [self.db], self.client) container = self.db.create_container() container.start() self.host_path = container.get_mount('/var/db')['Source']
def test_recreate_preserves_volume_with_trailing_slash(self): """When the Compose file specifies a trailing slash in the container path, make sure we copy the volume over when recreating. """ service = self.create_service('data', volumes=[VolumeSpec.parse('/data/')]) old_container = create_and_start_container(service) volume_path = old_container.get_mount('/data')['Source'] new_container = service.recreate_container(old_container) self.assertEqual(new_container.get_mount('/data')['Source'], volume_path)
def test_project_up(self): web = self.create_service('web') db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')]) project = Project('composetest', [web, db], self.client) project.start() self.assertEqual(len(project.containers()), 0) project.up(['db']) self.assertEqual(len(project.containers()), 1) self.assertEqual(len(db.containers()), 1) self.assertEqual(len(web.containers()), 0)
def get_named_volumes(services): volume_specs = [ VolumeSpec.parse(volume) for service in services.values() for volume in service.get('volumes', []) ] names = { spec.external for spec in volume_specs if spec.is_named_volume } return {name: {'external': True} for name in names}
def test_create_twice(self): web = self.create_service('web') db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')]) project = Project('composetest', [web, db], self.client) project.create(['db', 'web']) project.create(['db', 'web']) self.assertEqual(len(project.containers()), 0) self.assertEqual(len(project.containers(stopped=True)), 2) self.assertEqual(len(db.containers()), 0) self.assertEqual(len(db.containers(stopped=True)), 1) self.assertEqual(len(web.containers()), 0) self.assertEqual(len(web.containers(stopped=True)), 1)
def test_project_up_starts_links(self): console = self.create_service('console') db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')]) web = self.create_service('web', links=[(db, 'db')]) project = Project('composetest', [web, db, console], self.client) project.start() self.assertEqual(len(project.containers()), 0) project.up(['web']) self.assertEqual(len(project.containers()), 2) self.assertEqual(len(web.containers()), 1) self.assertEqual(len(db.containers()), 1) self.assertEqual(len(console.containers()), 0)
def test_create_with_special_volume_mode(self): self.mock_client.inspect_image.return_value = {'Id': 'imageid'} self.mock_client.create_container.return_value = {'Id': 'containerid'} volume = '/tmp:/foo:z' Service( 'web', client=self.mock_client, image='busybox', volumes=[VolumeSpec.parse(volume, True)], ).create_container() assert self.mock_client.create_container.call_count == 1 assert self.mock_client.create_host_config.call_args[1]['binds'] == [volume]
def test_recreate_preserves_volumes(self): web = self.create_service('web') db = self.create_service('db', volumes=[VolumeSpec.parse('/etc')]) project = Project('composetest', [web, db], self.client) project.start() self.assertEqual(len(project.containers()), 0) project.up(['db']) self.assertEqual(len(project.containers()), 1) old_db_id = project.containers()[0].id db_volume_path = project.containers()[0].get('Volumes./etc') project.up(strategy=ConvergenceStrategy.always) self.assertEqual(len(project.containers()), 2) db_container = [c for c in project.containers() if 'db' in c.name][0] self.assertNotEqual(db_container.id, old_db_id) self.assertEqual(db_container.get('Volumes./etc'), db_volume_path)
def test_execute_convergence_plan_when_image_volume_masks_config(self): service = self.create_service("db", build={"context": "tests/fixtures/dockerfile-with-volume"}) old_container = create_and_start_container(service) self.assertEqual([mount["Destination"] for mount in old_container.get("Mounts")], ["/data"]) volume_path = old_container.get_mount("/data")["Source"] service.options["volumes"] = [VolumeSpec.parse("/tmp:/data")] with mock.patch("compose.service.log") as mock_log: new_container, = service.execute_convergence_plan(ConvergencePlan("recreate", [old_container])) mock_log.warn.assert_called_once_with(mock.ANY) _, args, kwargs = mock_log.warn.mock_calls[0] self.assertIn('Service "db" is using volume "/data" from the previous container', args[0]) self.assertEqual([mount["Destination"] for mount in new_container.get("Mounts")], ["/data"]) self.assertEqual(new_container.get_mount("/data")["Source"], volume_path)
def test_get_container_create_options_with_different_host_path_in_container_json(self): service = Service( 'web', image='busybox', volumes=[VolumeSpec.parse('/host/path:/data')], client=self.mock_client, ) volume_name = 'abcdefff1234' self.mock_client.inspect_image.return_value = { 'Id': 'ababab', 'ContainerConfig': { 'Volumes': { '/data': {}, } } } self.mock_client.inspect_container.return_value = { 'Id': '123123123', 'Image': 'ababab', 'Mounts': [ { 'Destination': '/data', 'Source': '/mnt/sda1/host/path', 'Mode': '', 'RW': True, 'Driver': 'local', 'Name': volume_name, }, ] } service._get_container_create_options( override_options={}, number=1, previous_container=Container(self.mock_client, {'Id': '123123123'}), ) assert ( self.mock_client.create_host_config.call_args[1]['binds'] == ['{}:/data:rw'.format(volume_name)] )
def test_build_volume_binding(self): binding = build_volume_binding( VolumeSpec.parse('/outside:/inside', True)) assert binding == ('/inside', '/outside:/inside:rw')
def test_parse_volume_windows_internal_path_native(self): windows_path = 'C:\\Users\\reimu\\scarlet:C:\\scarlet\\app:ro' assert VolumeSpec._parse_win32(windows_path, False) == ('C:\\Users\\reimu\\scarlet', 'C:\\scarlet\\app', 'ro')
def test_parse_volume_spec_with_mode(self): spec = VolumeSpec.parse('external:interval:ro') assert spec == ('external', 'interval', 'ro') spec = VolumeSpec.parse('external:interval:z') assert spec == ('external', 'interval', 'z')
def test_parse_volume_spec_too_many_parts(self): with pytest.raises(ConfigurationError) as exc: VolumeSpec.parse('one:two:three:four') assert 'has incorrect format' in exc.exconly()
def test_parse_volume_windows_absolute_path(self): windows_path = "c:\\Users\\me\\Documents\\shiny\\config:\\opt\\shiny\\config:ro" assert VolumeSpec.parse(windows_path) == ( "/c/Users/me/Documents/shiny/config", "/opt/shiny/config", "ro")
def test_parse_volume_windows_absolute_path_normalized(self): windows_path = "c:\\Users\\me\\Documents\\shiny\\config:/opt/shiny/config:ro" assert VolumeSpec._parse_win32( windows_path, True) == ("/c/Users/me/Documents/shiny/config", "/opt/shiny/config", "ro")
def test_parse_volume_windows_just_drives_normalized(self): windows_path = 'E:\\:C:\\:ro' assert VolumeSpec._parse_win32(windows_path, True) == ('/e/', 'C:\\', 'ro')
def test_parse_volume_spec_internal_and_external(self): spec = VolumeSpec.parse('external:interval') assert spec == ('external', 'interval', 'rw')
def test_parse_volume_windows_internal_path_normalized(self): windows_path = 'C:\\Users\\reimu\\scarlet:C:\\scarlet\\app:ro' assert VolumeSpec._parse_win32(windows_path, True) == ('/c/Users/reimu/scarlet', 'C:\\scarlet\\app', 'ro')
def test_parse_volume_spec_only_one_path(self): spec = VolumeSpec.parse('/the/volume') assert spec == (None, '/the/volume', 'rw')
def test_create_container_with_unspecified_volume(self): service = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')]) container = service.create_container() container.start() self.assertIn('/var/db', container.get('Volumes'))
def test_parse_volume_windows_mixed_notations_normalized(self): windows_path = 'C:\\Foo:/root/foo' assert VolumeSpec._parse_win32(windows_path, True) == ('/c/Foo', '/root/foo', 'rw')
def test_parse_volume_windows_absolute_path_native(self): windows_path = "c:\\Users\\me\\Documents\\shiny\\config:/opt/shiny/config:ro" assert VolumeSpec._parse_win32( windows_path, False) == ("c:\\Users\\me\\Documents\\shiny\\config", "/opt/shiny/config", "ro")
def test_parse_volume_windows_just_drives_native(self): windows_path = 'E:\\:C:\\:ro' assert VolumeSpec._parse_win32(windows_path, False) == ('E:\\', 'C:\\', 'ro')
def test_parse_volume_windows_mixed_notations_native(self): windows_path = 'C:\\Foo:/root/foo' assert VolumeSpec._parse_win32(windows_path, False) == ('C:\\Foo', '/root/foo', 'rw')
def test_create_container_with_unspecified_volume(self): service = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')]) container = service.create_container() service.start_container(container) assert container.get_mount('/var/db')