diff --git a/compose/config/config.py b/compose/config/config.py index 4d32b50c4..b3e017789 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 diff --git a/compose/config/config_schema_v2.1.json b/compose/config/config_schema_v2.1.json index de4ddf250..980e4ba10 100644 --- a/compose/config/config_schema_v2.1.json +++ b/compose/config/config_schema_v2.1.json @@ -246,7 +246,8 @@ "name": {"type": "string"} }, "additionalProperties": false - } + }, + "labels": {"$ref": "#/definitions/list_or_dict"} }, "additionalProperties": false }, @@ -268,6 +269,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 8962a8920..796836fe7 100644 --- a/compose/network.py +++ b/compose/network.py @@ -15,7 +15,7 @@ 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): + ipam=None, external_name=None, internal=False, labels=None): self.client = client self.project = project self.name = name @@ -24,6 +24,7 @@ class Network(object): self.ipam = create_ipam_config_from_dict(ipam) self.external_name = external_name self.internal = internal + self.labels = labels def ensure(self): if self.external_name: @@ -70,6 +71,7 @@ class Network(object): options=self.driver_opts, ipam=self.ipam, internal=self.internal, + labels=self.labels, ) def remove(self): @@ -118,6 +120,7 @@ def build_networks(name, config_data, client): ipam=data.get('ipam'), external_name=data.get('external_name'), internal=data.get('internal'), + 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 2247ffff0..54737c7a7 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -22,6 +22,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 @@ -767,6 +768,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 4427fe6b9..149facfe8 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -821,6 +821,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)) @@ -847,6 +883,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 88b990e52..7a8832d20 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: