def test_invalid_list_of_strings_format(self): expected_error_msg = "Service 'web' configuration key 'command' contains 1" expected_error_msg += ", which is an invalid type, it should be a string" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( build_config_details({"web": {"build": ".", "command": [1]}}, "tests/fixtures/extends", "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_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( build_config_details( {"web": {"image": "busybox", "dockerfile": "Dockerfile.alt"}}, "working_dir", "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_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_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_load_throws_error_when_not_dict(self): with self.assertRaises(ConfigurationError): config.load( build_config_details( {'web': 'busybox:latest'}, 'working_dir', '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_invalid_environment_dict_key_raises_validation_error(self): expected_error_msg = "Service 'web' configuration key 'environment' contains an invalid type" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( build_config_details( {"web": {"image": "busybox", "environment": {"---": "nope"}}}, "working_dir", "filename.yml" ) )
def test_config_valid_service_names(self): for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']: config.load( build_config_details( {valid_name: {'image': 'busybox'}}, 'tests/fixtures/extends', 'common.yml' ) )
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( build_config_details( {1: {'image': 'busybox'}}, 'working_dir', 'filename.yml' ) )
def test_invalid_config_not_unique_items(self): expected_error_msg = "has non-unique elements" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails( {"web": {"build": ".", "devices": ["/dev/foo:/dev/foo", "/dev/foo:/dev/foo"]}}, "tests/fixtures/extends", "filename.yml", ) )
def test_extends_validation_sub_property_key(self): expected_error_msg = "Service 'web' configuration key 'extends' 'file' contains an invalid type" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails( {"web": {"image": "busybox", "extends": {"file": 1, "service": "web"}}}, "tests/fixtures/extends", "filename.yml", ) )
def test_config_invalid_service_names(self): with self.assertRaises(ConfigurationError): for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']: config.load( build_config_details( {invalid_name: {'image': 'busybox'}}, 'working_dir', 'filename.yml' ) )
def test_logs_warning_for_boolean_in_environment(self, mock_logging): expected_warning_msg = "Warning: There is a boolean value, True in the 'environment' key." config.load( build_config_details( {"web": {"image": "busybox", "environment": {"SHOW_STUFF": True}}}, "working_dir", "filename.yml" ) ) self.assertTrue(mock_logging.warn.called) self.assertTrue(expected_warning_msg in mock_logging.warn.call_args[0][0])
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_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( build_config_details( ['foo', 'lol'], 'tests/fixtures/extends', 'filename.yml' ) )
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( build_config_details( {'web': {'image': 'busybox', 'ports': ports}}, 'working_dir', 'filename.yml' ) )
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( build_config_details( {'web': {'image': 'busybox', 'dockerfile': 'Dockerfile.alt'}}, 'working_dir', 'filename.yml' ) )
def test_extends_validation_missing_service_key(self): with self.assertRaisesRegexp(ConfigurationError, "'service' is a required property"): config.load( build_config_details( { 'web': {'image': 'busybox', 'extends': {'file': 'common.yml'}}, }, 'tests/fixtures/extends', 'filename.yml' ) )
def test_nonexistent_path(self): with self.assertRaises(ConfigurationError): config.load( build_config_details( { 'foo': {'build': 'nonexistent.path'}, }, 'working_dir', 'filename.yml' ) )
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( build_config_details( {"web": {"image": "busybox", "extra_hosts": "somehost:162.242.195.82"}}, "working_dir", "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( build_config_details( {'web': {'image': 'busybox', 'ports': invalid_ports}}, 'working_dir', 'filename.yml' ) )
def test_extends_validation_empty_dictionary(self): with self.assertRaisesRegexp(ConfigurationError, 'service'): config.load( build_config_details( { '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( build_config_details( { 'foo': {'image': 'busybox', 'build': '.'}, }, 'tests/fixtures/extends', 'filename.yml' ) )
def test_config_hint(self): expected_error_msg = "(did you mean 'privileged'?)" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( build_config_details( { '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( build_config_details( { 'foo': {'image': 'busybox', 'links': 'an_link'}, }, 'tests/fixtures/extends', 'filename.yml' ) )
def test_invalid_config_not_unique_items(self): expected_error_msg = "has non-unique elements" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( build_config_details( { 'web': {'build': '.', 'devices': ['/dev/foo:/dev/foo', '/dev/foo:/dev/foo']} }, 'tests/fixtures/extends', 'filename.yml' ) )
def test_config_ulimits_required_keys_validation_error(self): expected_error_msg = "Service 'web' configuration key 'ulimits' u?'hard' is a required property" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( build_config_details( {'web': { 'image': 'busybox', 'ulimits': { 'nofile': { "soft": 10000, } } }}, 'working_dir', 'filename.yml' ) )
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 __init__(self, name, working_dir, config_file): config_file_path = os.path.join(working_dir, config_file) cfg_file = ConfigFile.from_filename(config_file_path) c = ConfigDetails( working_dir, [cfg_file], ) self.cd = load(c) self.name = name
def test_extends_validation_sub_property_key(self): expected_error_msg = "Service 'web' configuration key 'extends' 'file' contains an invalid type" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails( { 'web': { 'image': 'busybox', 'extends': { 'file': 1, 'service': 'web', } }, }, 'tests/fixtures/extends', 'filename.yml' ) )
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_config_ulimits_soft_greater_than_hard_error(self): expected_error_msg = "cannot contain a 'soft' value higher than 'hard' value" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( build_config_details( {'web': { 'image': 'busybox', 'ulimits': { 'nofile': { "soft": 10000, "hard": 1000 } } }}, 'working_dir', 'filename.yml' ) )
def test_volume_binding_with_environment_variable(self): os.environ['VOLUME_PATH'] = '/host/path' d = config.load( build_config_details( {'foo': {'build': '.', 'volumes': ['${VOLUME_PATH}:/container/path']}}, '.', None, ) )[0] self.assertEqual(d['volumes'], ['/host/path:/container/path'])
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_validation_fails_with_just_memswap_limit(self): """ When you set a 'memswap_limit' it is invalid config unless you also set a mem_limit """ expected_error_msg = ( "Invalid 'memswap_limit' configuration for 'foo' service: when " "defining 'memswap_limit' you must set 'mem_limit' as well" ) with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails( { 'foo': {'image': 'busybox', 'memswap_limit': 2000000}, }, 'tests/fixtures/extends', 'filename.yml' ) )
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 __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_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_extends_validation_invalid_key(self): expected_error_msg = "Unsupported config option for 'web' service: 'rogue_key'" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( config.ConfigDetails( { 'web': { 'image': 'busybox', 'extends': { 'file': 'common.yml', 'service': 'web', 'rogue_key': 'is not allowed' } }, }, 'tests/fixtures/extends', 'filename.yml' ) )
def test_config_ulimits_invalid_keys_validation_error(self): expected_error_msg = "Service 'web' configuration key 'ulimits' contains unsupported option: 'not_soft_or_hard'" with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( build_config_details( {'web': { 'image': 'busybox', 'ulimits': { 'nofile': { "not_soft_or_hard": 100, "soft": 10000, "hard": 20000, } } }}, 'working_dir', 'filename.yml' ) )
def test_extends_validation_sub_property_key(self): expected_error_msg = ( "Service 'web' configuration key 'extends' 'file' contains 1, " "which is an invalid type, it should be a string" ) with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( build_config_details( { 'web': { 'image': 'busybox', 'extends': { 'file': 1, 'service': 'web', } }, }, 'tests/fixtures/extends', 'filename.yml' ) )
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_extends_validation_valid_config(self): service = config.load( config.ConfigDetails( { 'web': {'image': 'busybox', 'extends': {'service': 'web', 'file': 'common.yml'}}, }, 'tests/fixtures/extends', 'common.yml' ) ) self.assertEquals(len(service), 1) self.assertIsInstance(service[0], dict)
def test_extends_validation_invalid_key(self): expected_error_msg = ( "Service 'web' configuration key 'extends' " "contains unsupported option: 'rogue_key'" ) with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): config.load( build_config_details( { 'web': { 'image': 'busybox', 'extends': { 'file': 'common.yml', 'service': 'web', 'rogue_key': 'is not allowed' } }, }, 'tests/fixtures/extends', 'filename.yml' ) )
def test_unset_variable_produces_warning(self): os.environ.pop('FOO', None) os.environ.pop('BAR', None) config_details = config.ConfigDetails( config={ 'web': { 'image': '${FOO}', 'command': '${BAR}', 'container_name': '${BAR}', }, }, working_dir='.', filename=None, ) with mock.patch('compose.config.interpolation.log') as log: config.load(config_details) self.assertEqual(2, log.warn.call_count) warnings = sorted(args[0][0] for args in log.warn.call_args_list) self.assertIn('BAR', warnings[0]) self.assertIn('FOO', warnings[1])
def test_project_up_logging_with_multiple_files(self): base_file = config.ConfigFile( 'base.yml', { 'version': V2_0, 'services': { 'simple': {'image': 'busybox:latest', 'command': 'top'}, 'another': { 'image': 'busybox:latest', 'command': 'top', 'logging': { 'driver': "json-file", 'options': { 'max-size': "10m" } } } } }) override_file = config.ConfigFile( 'override.yml', { 'version': V2_0, 'services': { 'another': { 'logging': { 'driver': "none" } } } }) details = config.ConfigDetails('.', [base_file, override_file]) tmpdir = py.test.ensuretemp('logging_test') self.addCleanup(tmpdir.remove) with tmpdir.as_cwd(): 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), 2) another = project.get_service('another').containers()[0] log_config = another.get('HostConfig.LogConfig') self.assertTrue(log_config) self.assertEqual(log_config.get('Type'), 'none')
def test_empty_environment_key_allowed(self): service_dict = config.load( build_config_details( { 'web': { 'build': '.', 'environment': { 'POSTGRES_PASSWORD': '' }, }, }, '.', None, ) )[0] self.assertEquals(service_dict['environment']['POSTGRES_PASSWORD'], '')
def for_target(self, target): config_files = [ self.base_config(target), self.target_config(target), self.workspace_config(target) ] config_details = compose.ConfigDetails(self.working_dir, config_files) target_yml = self.target_yml(target) construi = { 'before': target_yml['before'] if 'before' in target_yml else [], 'name': target, 'run': self.target_yml(target).get('run', []), 'project_name': self.project_name } return TargetConfig(construi, compose.load(config_details))
def test_config_file_with_environment_variable(self): os.environ.update( IMAGE="busybox", HOST_PORT="80", LABEL_VALUE="myvalue", ) service_dicts = config.load( config.find('tests/fixtures/environment-interpolation', None), ) self.assertEqual(service_dicts, [{ 'name': 'web', 'image': 'busybox', 'ports': ['80:8000'], 'labels': { 'mylabel': 'myvalue' }, 'hostname': 'host-', 'command': '${ESCAPED}', }])
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_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_load_with_multiple_files_and_extends_in_override_file(self): base_file = config.ConfigFile( 'base.yaml', { 'web': {'image': 'example/web'}, }) override_file = config.ConfigFile( 'override.yaml', { 'web': { 'extends': { 'file': 'common.yml', 'service': 'base', }, 'volumes': ['/home/user/project:/code'], }, }) details = config.ConfigDetails('.', [base_file, override_file]) tmpdir = py.test.ensuretemp('config_test') tmpdir.join('common.yml').write(""" base: labels: ['label=one'] """) with tmpdir.as_cwd(): service_dicts = config.load(details) expected = [ { 'name': 'web', 'image': 'example/web', 'volumes': ['/home/user/project:/code'], 'labels': {'label': 'one'}, }, ] self.assertEqual(service_sort(service_dicts), service_sort(expected))
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 build_service_dicts(service_config): return config.load( config.ConfigDetails('working_dir', [config.ConfigFile(None, service_config)]))
def build_config(contents, **kwargs): return load(build_config_details(contents, **kwargs))
def load_from_filename(filename): return config.load(config.find('.', filename))