Browse Source

Merge pull request #1269 from aanand/labels

Implement 'labels' option
Daniel Nephin 10 years ago
parent
commit
6c95eed781

+ 1 - 1
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)

+ 40 - 1
compose/config.py

@@ -19,6 +19,7 @@ DOCKER_CONFIG_KEYS = [
     'extra_hosts',
     'hostname',
     'image',
+    'labels',
     'links',
     'mem_limit',
     'net',
@@ -182,6 +183,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
 
 
@@ -200,6 +204,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']
 
@@ -218,7 +228,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:
@@ -389,6 +399,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))
 

+ 4 - 0
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:

+ 2 - 2
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

+ 1 - 1
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/)

+ 18 - 0
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.

+ 1 - 1
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

+ 1 - 1
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',
 ]

+ 27 - 0
tests/integration/service_test.py

@@ -595,3 +595,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)

+ 41 - 0
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 = [