Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compose/cli/docker_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
41 changes: 40 additions & 1 deletion compose/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
'extra_hosts',
'hostname',
'image',
'labels',
'links',
'mem_limit',
'net',
Expand Down Expand Up @@ -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


Expand All @@ -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']

Expand All @@ -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:
Expand Down Expand Up @@ -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))

Expand Down
4 changes: 4 additions & 0 deletions compose/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ def format_port(private, public):
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:
Expand Down
4 changes: 2 additions & 2 deletions docs/extends.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down
18 changes: 18 additions & 0 deletions docs/yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def find_version(*file_paths):
'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',
]
Expand Down
27 changes: 27 additions & 0 deletions tests/integration/service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,3 +584,30 @@ def test_resolve_env(self):
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 changes: 41 additions & 0 deletions tests/unit/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,47 @@ def test_add_list(self):
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 = [
Expand Down