def test_resolve_path(self): os.environ['HOSTENV'] = '/tmp' os.environ['CONTAINERENV'] = '/host/tmp' service_dict = config.load( config.ConfigDetails( config={ 'foo': { 'build': '.', 'volumes': ['$HOSTENV:$CONTAINERENV'] } }, working_dir="tests/fixtures/env", filename=None, ))[0] self.assertEqual(set(service_dict['volumes']), set(['/tmp:/host/tmp'])) service_dict = config.load( config.ConfigDetails( config={ 'foo': { 'build': '.', 'volumes': ['/opt${HOSTENV}:/opt${CONTAINERENV}'] } }, working_dir="tests/fixtures/env", filename=None, ))[0] self.assertEqual(set(service_dict['volumes']), set(['/opt/tmp:/opt/host/tmp']))
def test_load_with_multiple_files(self): base_file = config.ConfigFile( 'base.yaml', { 'web': { 'image': 'example/web', 'links': ['db'], }, 'db': { 'image': 'example/db', }, }) override_file = config.ConfigFile('override.yaml', { 'web': { 'build': '/', 'volumes': ['/home/user/project:/code'], }, }) details = config.ConfigDetails('.', [base_file, override_file]) service_dicts = config.load(details) expected = [ { 'name': 'web', 'build': '/', 'links': ['db'], 'volumes': ['/home/user/project:/code'], }, { 'name': 'db', 'image': 'example/db', }, ] self.assertEqual(service_sort(service_dicts), service_sort(expected))
def test_project_up_named_volumes_in_binds(self): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) base_file = config.ConfigFile( 'base.yml', { 'version': V2_0, 'services': { 'simple': { 'image': 'busybox:latest', 'command': 'top', 'volumes': ['{0}:/data'.format(vol_name)] }, }, 'volumes': { vol_name: {'driver': 'local'} } }) config_details = config.ConfigDetails('.', [base_file]) config_data = config.load(config_details) project = Project.from_config( name='composetest', config_data=config_data, client=self.client ) service = project.services[0] self.assertEqual(service.name, 'simple') volumes = service.options.get('volumes') self.assertEqual(len(volumes), 1) self.assertEqual(volumes[0].external, full_vol_name) project.up() engine_volumes = self.client.volumes()['Volumes'] container = service.get_container() assert [mount['Name'] for mount in container.get('Mounts')] == [full_vol_name] assert next((v for v in engine_volumes if v['Name'] == vol_name), None) is None
def test_config_invalid_service_names(self): with self.assertRaises(ConfigurationError): for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']: config.load( config.ConfigDetails({invalid_name: { 'image': 'busybox' }}, 'working_dir', 'filename.yml'))
def test_project_up_port_mappings_with_multiple_files(self): base_file = config.ConfigFile( 'base.yml', { 'version': V2_0, 'services': { 'simple': { 'image': 'busybox:latest', 'command': 'top', 'ports': ['1234:1234'] }, }, }) override_file = config.ConfigFile('override.yml', { 'version': V2_0, 'services': { 'simple': { 'ports': ['1234:1234'] } } }) details = config.ConfigDetails('.', [base_file, override_file]) config_data = config.load(details) project = Project.from_config(name='composetest', config_data=config_data, client=self.client) project.up() containers = project.containers() self.assertEqual(len(containers), 1)
def test_config_integer_service_name_raise_validation_error(self): expected_error_msg = "Service name: 1 needs to be a string, eg '1'" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails({1: { 'image': 'busybox' }}, 'working_dir', 'filename.yml'))
def test_load(self): service_dicts = config.load( config.ConfigDetails( { 'foo': { 'image': 'busybox' }, 'bar': { 'image': 'busybox', 'environment': ['FOO=1'] }, }, 'tests/fixtures/extends', 'common.yml')) self.assertEqual( service_sort(service_dicts), service_sort([{ 'name': 'bar', 'image': 'busybox', 'environment': { 'FOO': '1' }, }, { 'name': 'foo', 'image': 'busybox', }]))
def test_invalid_list_of_strings_format(self): expected_error_msg = "'command' contains an invalid type, valid types are string or array" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails({'web': { 'build': '.', 'command': [1] }}, 'tests/fixtures/extends', 'filename.yml'))
def test_nonexistent_path(self): with self.assertRaises(ConfigurationError): config.load( config.ConfigDetails({ 'foo': { 'build': 'nonexistent.path' }, }, 'working_dir', 'filename.yml'))
def test_config_valid_service_names(self): for valid_name in [ '_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup' ]: config.load( config.ConfigDetails({valid_name: { 'image': 'busybox' }}, 'tests/fixtures/extends', 'common.yml'))
def make_project(self, cfg): details = config.ConfigDetails( 'working_dir', [config.ConfigFile(None, cfg)]) return Project.from_config( name='composetest', client=self.client, config_data=config.load(details))
def test_memswap_can_be_a_string(self): service_dict = config.load( config.ConfigDetails( {'foo': {'image': 'busybox', 'mem_limit': "1G", 'memswap_limit': "512M"}}, 'tests/fixtures/extends', 'common.yml' ) ) self.assertEqual(service_dict[0]['memswap_limit'], "512M")
def test_validation_with_correct_memswap_values(self): service_dict = config.load( config.ConfigDetails( {'foo': {'image': 'busybox', 'mem_limit': 1000000, 'memswap_limit': 2000000}}, 'tests/fixtures/extends', 'common.yml' ) ) self.assertEqual(service_dict[0]['memswap_limit'], 2000000)
def test_load_throws_error_when_not_dict(self): with self.assertRaises(ConfigurationError): config.load( config.ConfigDetails( {'web': 'busybox:latest'}, 'working_dir', 'filename.yml' ) )
def test_load_with_multiple_files_and_empty_base(self): base_file = config.ConfigFile('base.yaml', None) override_file = config.ConfigFile( 'override.yaml', {'web': {'image': 'example/web'}}) details = config.ConfigDetails('.', [base_file, override_file]) with pytest.raises(ConfigurationError) as exc: config.load(details) assert 'Top level object needs to be a dictionary' in exc.exconly()
def test_volume_binding_with_environment_variable(self): os.environ['VOLUME_PATH'] = '/host/path' d = config.load( config.ConfigDetails( config={'foo': {'build': '.', 'volumes': ['${VOLUME_PATH}:/container/path']}}, working_dir='.', filename=None, ) )[0] self.assertEqual(d['volumes'], ['/host/path:/container/path'])
def test_valid_config_oneof_string_or_list(self): entrypoint_values = [["sh"], "sh"] for entrypoint in entrypoint_values: service = config.load( config.ConfigDetails( {'web': { 'image': 'busybox', 'entrypoint': entrypoint }}, 'working_dir', 'filename.yml')) self.assertEqual(service[0]['entrypoint'], entrypoint)
def test_invalid_config_not_a_dictionary(self): expected_error_msg = "Top level object needs to be a dictionary." with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails( ['foo', 'lol'], 'tests/fixtures/extends', 'filename.yml' ) )
def test_valid_config_which_allows_two_type_definitions(self): expose_values = [["8000"], [8000]] for expose in expose_values: service = config.load( config.ConfigDetails( {'web': { 'image': 'busybox', 'expose': expose }}, 'working_dir', 'filename.yml')) self.assertEqual(service[0]['expose'], expose)
def test_config_image_and_dockerfile_raise_validation_error(self): expected_error_msg = "Service 'web' has both an image and alternate Dockerfile." with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails( {'web': {'image': 'busybox', 'dockerfile': 'Dockerfile.alt'}}, 'working_dir', 'filename.yml' ) )
def __get_compose_project(self): client = docker_client(Environment()) config_data = config.load( config.ConfigDetails( self.home_path, [config.ConfigFile.from_filename(self.compose_file)])) return DockerComposeProject.from_config(name='metal', client=client, config_data=config_data)
def test_config_valid_ports_format_validation(self): valid_ports = [["8000", "9000"], ["8000/8050"], ["8000"], [8000], ["49153-49154:3002-3003"]] for ports in valid_ports: config.load( config.ConfigDetails( {'web': { 'image': 'busybox', 'ports': ports }}, 'working_dir', 'filename.yml'))
def test_extends_validation_empty_dictionary(self): with self.assertRaisesRegexp(ConfigurationError, 'service'): config.load( config.ConfigDetails( { 'web': { 'image': 'busybox', 'extends': {} }, }, 'tests/fixtures/extends', 'filename.yml'))
def test_invalid_config_build_and_image_specified(self): expected_error_msg = "Service 'foo' has both an image and build path specified." with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails( { 'foo': { 'image': 'busybox', 'build': '.' }, }, 'tests/fixtures/extends', 'filename.yml'))
def test_extends_validation_missing_service_key(self): with self.assertRaisesRegexp(ConfigurationError, "'service' is a required property"): config.load( config.ConfigDetails( { 'web': {'image': 'busybox', 'extends': {'file': 'common.yml'}}, }, 'tests/fixtures/extends', 'filename.yml' ) )
def test_config_invalid_ports_format_validation(self): expected_error_msg = "Service 'web' configuration key 'ports' contains an invalid type" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): for invalid_ports in [{"1": "8000"}, False, 0, "8000", 8000, ["8000", "8000"]]: config.load( config.ConfigDetails( {'web': {'image': 'busybox', 'ports': invalid_ports}}, 'working_dir', 'filename.yml' ) )
def test_config_hint(self): expected_error_msg = "(did you mean 'privileged'?)" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails( { 'foo': { 'image': 'busybox', 'privilige': 'something' }, }, 'tests/fixtures/extends', 'filename.yml'))
def test_invalid_config_type_should_be_an_array(self): expected_error_msg = "Service 'foo' configuration key 'links' contains an invalid type, it should be an array" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails( { 'foo': { 'image': 'busybox', 'links': 'an_link' }, }, 'tests/fixtures/extends', 'filename.yml'))
def test_invalid_interpolation(self): with self.assertRaises(config.ConfigurationError) as cm: config.load( config.ConfigDetails({'web': { 'image': '${' }}, 'working_dir', 'filename.yml')) self.assertIn('Invalid', cm.exception.msg) self.assertIn('for "image" option', cm.exception.msg) self.assertIn('in service "web"', cm.exception.msg) self.assertIn('"${"', cm.exception.msg)
def test_config_extra_hosts_string_raises_validation_error(self): expected_error_msg = "Service 'web' configuration key 'extra_hosts' contains an invalid type" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails( { 'web': { 'image': 'busybox', 'extra_hosts': 'somehost:162.242.195.82' } }, 'working_dir', 'filename.yml'))