From 2e6bc078fbc502965ac5d3a3ec0be13aafcfeb09 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 19 Mar 2015 16:10:27 -0700 Subject: [PATCH] Implement 'labels' option Signed-off-by: Aanand Prasad --- compose/cli/docker_client.py | 2 +- compose/config.py | 41 ++++++++++++++++++++++++++++++- compose/container.py | 4 +++ docs/extends.md | 4 +-- docs/install.md | 2 +- docs/yml.md | 18 ++++++++++++++ requirements.txt | 2 +- setup.py | 2 +- tests/integration/service_test.py | 27 ++++++++++++++++++++ tests/unit/config_test.py | 41 +++++++++++++++++++++++++++++++ 10 files changed, 136 insertions(+), 7 deletions(-) diff --git a/compose/cli/docker_client.py b/compose/cli/docker_client.py index 7bbe0ebfa..e513182fb 100644 --- a/compose/cli/docker_client.py +++ b/compose/cli/docker_client.py @@ -32,4 +32,4 @@ def docker_client(): ) timeout = int(os.environ.get('DOCKER_CLIENT_TIMEOUT', 60)) - return Client(base_url=base_url, tls=tls_config, version='1.17', timeout=timeout) + return Client(base_url=base_url, tls=tls_config, version='1.18', timeout=timeout) diff --git a/compose/config.py b/compose/config.py index c28703961..d5a82114a 100644 --- a/compose/config.py +++ b/compose/config.py @@ -18,6 +18,7 @@ DOCKER_CONFIG_KEYS = [ 'extra_hosts', 'hostname', 'image', + 'labels', 'links', 'mem_limit', 'net', @@ -180,6 +181,9 @@ def process_container_options(service_dict, working_dir=None): if 'build' in service_dict: service_dict['build'] = resolve_build_path(service_dict['build'], working_dir=working_dir) + if 'labels' in service_dict: + service_dict['labels'] = parse_labels(service_dict['labels']) + return service_dict @@ -198,6 +202,12 @@ def merge_service_dicts(base, override): override.get('volumes'), ) + if 'labels' in base or 'labels' in override: + d['labels'] = merge_labels( + base.get('labels'), + override.get('labels'), + ) + if 'image' in override and 'build' in d: del d['build'] @@ -216,7 +226,7 @@ def merge_service_dicts(base, override): if key in base or key in override: d[key] = to_list(base.get(key)) + to_list(override.get(key)) - already_merged_keys = ['environment', 'volumes'] + list_keys + list_or_string_keys + already_merged_keys = ['environment', 'volumes', 'labels'] + list_keys + list_or_string_keys for k in set(ALLOWED_KEYS) - set(already_merged_keys): if k in override: @@ -385,6 +395,35 @@ def join_volume(pair): return ":".join((host, container)) +def merge_labels(base, override): + labels = parse_labels(base) + labels.update(parse_labels(override)) + return labels + + +def parse_labels(labels): + if not labels: + return {} + + if isinstance(labels, list): + return dict(split_label(e) for e in labels) + + if isinstance(labels, dict): + return labels + + raise ConfigurationError( + "labels \"%s\" must be a list or mapping" % + labels + ) + + +def split_label(label): + if '=' in label: + return label.split('=', 1) + else: + return label, '' + + def expand_path(working_dir, path): return os.path.abspath(os.path.join(working_dir, path)) diff --git a/compose/container.py b/compose/container.py index e10f13850..9439a7087 100644 --- a/compose/container.py +++ b/compose/container.py @@ -79,6 +79,10 @@ class Container(object): return ', '.join(format_port(*item) for item in sorted(six.iteritems(self.ports))) + @property + def labels(self): + return self.get('Config.Labels') or {} + @property def human_readable_state(self): if self.is_running: diff --git a/docs/extends.md b/docs/extends.md index 2393ca6ae..06c08f25e 100644 --- a/docs/extends.md +++ b/docs/extends.md @@ -321,8 +321,8 @@ expose: - "5000" ``` -In the case of `environment`, Compose "merges" entries together with -locally-defined values taking precedence: +In the case of `environment` and `labels`, Compose "merges" entries together +with locally-defined values taking precedence: ```yaml # original service diff --git a/docs/install.md b/docs/install.md index 24928d74c..a3524c603 100644 --- a/docs/install.md +++ b/docs/install.md @@ -10,7 +10,7 @@ Compose with a `curl` command. ### Install Docker -First, install Docker version 1.3 or greater: +First, install Docker version 1.6 or greater: - [Instructions for Mac OS X](http://docs.docker.com/installation/mac/) - [Instructions for Ubuntu](http://docs.docker.com/installation/ubuntulinux/) diff --git a/docs/yml.md b/docs/yml.md index 101c2cf27..964cf5f20 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -253,6 +253,24 @@ environment variables (DEBUG) with a new value, and the other one For more on `extends`, see the [tutorial](extends.md#example) and [reference](extends.md#reference). +### labels + +Add metadata to containers using [Docker labels](http://docs.docker.com/userguide/labels-custom-metadata/). You can use either an array or a dictionary. + +It's recommended that you use reverse-DNS notation to prevent your labels from conflicting with those used by other software. + +``` +labels: + com.example.description: "Accounting webapp" + com.example.department: "Finance" + com.example.label-with-empty-value: "" + +labels: + - "com.example.description=Accounting webapp" + - "com.example.department=Finance" + - "com.example.label-with-empty-value" +``` + ### net Networking mode. Use the same values as the docker client `--net` parameter. diff --git a/requirements.txt b/requirements.txt index 65f075442..ed09cccac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ PyYAML==3.10 -docker-py==1.1.0 +docker-py==1.2.1 dockerpty==0.3.2 docopt==0.6.1 requests==2.6.1 diff --git a/setup.py b/setup.py index c02a31f4f..46193eeef 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ install_requires = [ 'requests >= 2.6.1, < 2.7', 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.11.0, < 1.0', - 'docker-py >= 1.1.0, < 1.2', + 'docker-py >= 1.2.0, < 1.3', 'dockerpty >= 0.3.2, < 0.4', 'six >= 1.3.0, < 2', ] diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 3fbf546c0..df5b2b9d3 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -584,3 +584,30 @@ class ServiceTest(DockerClientTestCase): env = create_and_start_container(service).environment for k, v in {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}.items(): self.assertEqual(env[k], v) + + def test_labels(self): + labels_dict = { + 'com.example.description': "Accounting webapp", + 'com.example.department': "Finance", + 'com.example.label-with-empty-value': "", + } + + service = self.create_service('web', labels=labels_dict) + labels = create_and_start_container(service).labels.items() + for pair in labels_dict.items(): + self.assertIn(pair, labels) + + labels_list = ["%s=%s" % pair for pair in labels_dict.items()] + + service = self.create_service('web', labels=labels_list) + labels = create_and_start_container(service).labels.items() + for pair in labels_dict.items(): + self.assertIn(pair, labels) + + def test_empty_labels(self): + labels_list = ['foo', 'bar'] + + service = self.create_service('web', labels=labels_list) + labels = create_and_start_container(service).labels.items() + for name in labels_list: + self.assertIn((name, ''), labels) diff --git a/tests/unit/config_test.py b/tests/unit/config_test.py index 97bd1b91d..c478a2182 100644 --- a/tests/unit/config_test.py +++ b/tests/unit/config_test.py @@ -185,6 +185,47 @@ class MergeStringsOrListsTest(unittest.TestCase): self.assertEqual(set(service_dict['dns']), set(['8.8.8.8', '9.9.9.9'])) +class MergeLabelsTest(unittest.TestCase): + def test_empty(self): + service_dict = config.merge_service_dicts({}, {}) + self.assertNotIn('labels', service_dict) + + def test_no_override(self): + service_dict = config.merge_service_dicts( + config.make_service_dict('foo', {'labels': ['foo=1', 'bar']}), + config.make_service_dict('foo', {}), + ) + self.assertEqual(service_dict['labels'], {'foo': '1', 'bar': ''}) + + def test_no_base(self): + service_dict = config.merge_service_dicts( + config.make_service_dict('foo', {}), + config.make_service_dict('foo', {'labels': ['foo=2']}), + ) + self.assertEqual(service_dict['labels'], {'foo': '2'}) + + def test_override_explicit_value(self): + service_dict = config.merge_service_dicts( + config.make_service_dict('foo', {'labels': ['foo=1', 'bar']}), + config.make_service_dict('foo', {'labels': ['foo=2']}), + ) + self.assertEqual(service_dict['labels'], {'foo': '2', 'bar': ''}) + + def test_add_explicit_value(self): + service_dict = config.merge_service_dicts( + config.make_service_dict('foo', {'labels': ['foo=1', 'bar']}), + config.make_service_dict('foo', {'labels': ['bar=2']}), + ) + self.assertEqual(service_dict['labels'], {'foo': '1', 'bar': '2'}) + + def test_remove_explicit_value(self): + service_dict = config.merge_service_dicts( + config.make_service_dict('foo', {'labels': ['foo=1', 'bar=2']}), + config.make_service_dict('foo', {'labels': ['bar']}), + ) + self.assertEqual(service_dict['labels'], {'foo': '1', 'bar': ''}) + + class EnvTest(unittest.TestCase): def test_parse_environment_as_list(self): environment = [