diff --git a/compose/config/config.py b/compose/config/config.py index be73e1dee..870bbad9c 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -361,6 +361,9 @@ def load_mapping(config_files, get_func, entity_type): config['driver_opts'] ) + if 'labels' in config: + config['labels'] = parse_labels(config['labels']) + return mapping @@ -416,10 +419,11 @@ def load_services(config_details, config_file): def interpolate_config_section(config_file, config, section, environment): validate_config_section(config_file.filename, config, section) return interpolate_environment_variables( - config_file.version, - config, - section, - environment) + config_file.version, + config, + section, + environment + ) def process_config_file(config_file, environment, service_name=None): diff --git a/compose/config/config_schema_v2.1.json b/compose/config/config_schema_v2.1.json index 24c0c9bdc..f30b90543 100644 --- a/compose/config/config_schema_v2.1.json +++ b/compose/config/config_schema_v2.1.json @@ -248,7 +248,8 @@ }, "additionalProperties": false }, - "enable_ipv6": {"type": "boolean"} + "enable_ipv6": {"type": "boolean"}, + "labels": {"$ref": "#/definitions/list_or_dict"} }, "additionalProperties": false }, @@ -270,6 +271,7 @@ "name": {"type": "string"} } }, + "labels": {"$ref": "#/definitions/list_or_dict"}, "additionalProperties": false }, "additionalProperties": false diff --git a/compose/network.py b/compose/network.py index c3af9aa1d..00d68aa4f 100644 --- a/compose/network.py +++ b/compose/network.py @@ -15,7 +15,8 @@ log = logging.getLogger(__name__) class Network(object): def __init__(self, client, project, name, driver=None, driver_opts=None, - ipam=None, external_name=None, internal=False, enable_ipv6=False): + ipam=None, external_name=None, internal=False, enable_ipv6=False, + labels=None): self.client = client self.project = project self.name = name @@ -71,7 +72,8 @@ class Network(object): options=self.driver_opts, ipam=self.ipam, internal=self.internal, - enable_ipv6=self.enable_ipv6 + enable_ipv6=self.enable_ipv6, + labels=self.labels, ) def remove(self): @@ -121,6 +123,7 @@ def build_networks(name, config_data, client): external_name=data.get('external_name'), internal=data.get('internal'), enable_ipv6=data.get('enable_ipv6'), + labels=data.get('labels'), ) for network_name, data in network_config.items() } diff --git a/compose/volume.py b/compose/volume.py index f440ba40c..1fd1d51c9 100644 --- a/compose/volume.py +++ b/compose/volume.py @@ -12,17 +12,18 @@ log = logging.getLogger(__name__) class Volume(object): def __init__(self, client, project, name, driver=None, driver_opts=None, - external_name=None): + external_name=None, labels=None): self.client = client self.project = project self.name = name self.driver = driver self.driver_opts = driver_opts self.external_name = external_name + self.labels = labels def create(self): return self.client.create_volume( - self.full_name, self.driver, self.driver_opts + self.full_name, self.driver, self.driver_opts, labels=self.labels ) def remove(self): @@ -68,7 +69,8 @@ class ProjectVolumes(object): name=vol_name, driver=data.get('driver'), driver_opts=data.get('driver_opts'), - external_name=data.get('external_name') + external_name=data.get('external_name'), + labels=data.get('labels') ) for vol_name, data in config_volumes.items() } diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index e2c027980..a7cd78f18 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -24,6 +24,7 @@ from compose.project import OneOffFilter from tests.integration.testcases import DockerClientTestCase from tests.integration.testcases import get_links from tests.integration.testcases import pull_busybox +from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_only @@ -773,6 +774,46 @@ class CLITestCase(DockerClientTestCase): container = self.project.containers()[0] assert list(container.get('NetworkSettings.Networks')) == [network_name] + @v2_1_only() + def test_up_with_network_labels(self): + filename = 'network-label.yml' + + self.base_dir = 'tests/fixtures/networks' + self._project = get_project(self.base_dir, [filename]) + + self.dispatch(['-f', filename, 'up', '-d'], returncode=0) + + network_with_label = '{}_network_with_label'.format(self.project.name) + + networks = [ + n for n in self.client.networks() + if n['Name'].startswith('{}_'.format(self.project.name)) + ] + + assert [n['Name'] for n in networks] == [network_with_label] + + assert networks[0]['Labels'] == {'label_key': 'label_val'} + + @v2_1_only() + def test_up_with_volume_labels(self): + filename = 'volume-label.yml' + + self.base_dir = 'tests/fixtures/volumes' + self._project = get_project(self.base_dir, [filename]) + + self.dispatch(['-f', filename, 'up', '-d'], returncode=0) + + volume_with_label = '{}_volume_with_label'.format(self.project.name) + + volumes = [ + v for v in self.client.volumes().get('Volumes', []) + if v['Name'].startswith('{}_'.format(self.project.name)) + ] + + assert [v['Name'] for v in volumes] == [volume_with_label] + + assert volumes[0]['Labels'] == {'label_key': 'label_val'} + @v2_only() def test_up_no_services(self): self.base_dir = 'tests/fixtures/no-services' diff --git a/tests/fixtures/networks/network-label.yml b/tests/fixtures/networks/network-label.yml new file mode 100644 index 000000000..fdb24f652 --- /dev/null +++ b/tests/fixtures/networks/network-label.yml @@ -0,0 +1,13 @@ +version: "2.1" + +services: + web: + image: busybox + command: top + networks: + - network_with_label + +networks: + network_with_label: + labels: + - "label_key=label_val" diff --git a/tests/fixtures/volumes/volume-label.yml b/tests/fixtures/volumes/volume-label.yml new file mode 100644 index 000000000..a5f33a5aa --- /dev/null +++ b/tests/fixtures/volumes/volume-label.yml @@ -0,0 +1,13 @@ +version: "2.1" + +services: + web: + image: busybox + command: top + volumes: + - volume_with_label:/data + +volumes: + volume_with_label: + labels: + - "label_key=label_val" diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 5eb994f16..e3c34af9c 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -909,6 +909,42 @@ class ProjectTest(DockerClientTestCase): assert network['Internal'] is True + @v2_1_only() + def test_project_up_with_network_label(self): + self.require_api_version('1.23') + + network_name = 'network_with_label' + + config_data = config.Config( + version=V2_0, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'networks': {network_name: None} + }], + volumes={}, + networks={ + network_name: {'labels': {'label_key': 'label_val'}} + } + ) + + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data + ) + + project.up() + + networks = [ + n for n in self.client.networks() + if n['Name'].startswith('composetest_') + ] + + assert [n['Name'] for n in networks] == ['composetest_{}'.format(network_name)] + + assert networks[0]['Labels'] == {'label_key': 'label_val'} + @v2_only() def test_project_up_volumes(self): vol_name = '{0:x}'.format(random.getrandbits(32)) @@ -935,6 +971,46 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(volume_data['Name'], full_vol_name) self.assertEqual(volume_data['Driver'], 'local') + @v2_1_only() + def test_project_up_with_volume_labels(self): + self.require_api_version('1.23') + + volume_name = 'volume_with_label' + + config_data = config.Config( + version=V2_0, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'volumes': [VolumeSpec.parse('{}:/data'.format(volume_name))] + }], + volumes={ + volume_name: { + 'labels': { + 'label_key': 'label_val' + } + } + }, + networks={}, + ) + + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data, + ) + + project.up() + + volumes = [ + v for v in self.client.volumes().get('Volumes', []) + if v['Name'].startswith('composetest_') + ] + + assert [v['Name'] for v in volumes] == ['composetest_{}'.format(volume_name)] + + assert volumes[0]['Labels'] == {'label_key': 'label_val'} + @v2_only() def test_project_up_logging_with_multiple_files(self): base_file = config.ConfigFile( diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index d6bebbfcf..d205e2825 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -376,6 +376,59 @@ class ConfigTest(unittest.TestCase): } } + def test_load_config_volume_and_network_labels(self): + base_file = config.ConfigFile( + 'base.yaml', + { + 'version': '2.1', + 'services': { + 'web': { + 'image': 'example/web', + }, + }, + 'networks': { + 'with_label': { + 'labels': { + 'label_key': 'label_val' + } + } + }, + 'volumes': { + 'with_label': { + 'labels': { + 'label_key': 'label_val' + } + } + } + } + ) + + details = config.ConfigDetails('.', [base_file]) + network_dict = config.load(details).networks + volume_dict = config.load(details).volumes + + self.assertEqual( + network_dict, + { + 'with_label': { + 'labels': { + 'label_key': 'label_val' + } + } + } + ) + + self.assertEqual( + volume_dict, + { + 'with_label': { + 'labels': { + 'label_key': 'label_val' + } + } + } + ) + def test_load_config_invalid_service_names(self): for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']: with pytest.raises(ConfigurationError) as exc: