Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for expanded port syntax in 3.1 format #4541

Merged
merged 1 commit into from
Mar 7, 2017
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
36 changes: 35 additions & 1 deletion compose/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .types import parse_extra_hosts
from .types import parse_restart_spec
from .types import ServiceLink
from .types import ServicePort
from .types import VolumeFromSpec
from .types import VolumeSpec
from .validation import match_named_volumes
Expand Down Expand Up @@ -683,10 +684,25 @@ def process_service(service_config):
service_dict[field] = to_list(service_dict[field])

service_dict = process_healthcheck(service_dict, service_config.name)
service_dict = process_ports(service_dict)

return service_dict


def process_ports(service_dict):
if 'ports' not in service_dict:
return service_dict

ports = []
for port_definition in service_dict['ports']:
if isinstance(port_definition, ServicePort):
ports.append(port_definition)
else:
ports.extend(ServicePort.parse(port_definition))
service_dict['ports'] = ports
return service_dict


def process_depends_on(service_dict):
if 'depends_on' in service_dict and not isinstance(service_dict['depends_on'], dict):
service_dict['depends_on'] = dict([
Expand Down Expand Up @@ -864,7 +880,7 @@ def merge_service_dicts(base, override, version):
md.merge_field(field, merge_path_mappings)

for field in [
'ports', 'cap_add', 'cap_drop', 'expose', 'external_links',
'cap_add', 'cap_drop', 'expose', 'external_links',
'security_opt', 'volumes_from',
]:
md.merge_field(field, merge_unique_items_lists, default=[])
Expand All @@ -873,6 +889,7 @@ def merge_service_dicts(base, override, version):
md.merge_field(field, merge_list_or_string)

md.merge_field('logging', merge_logging, default={})
merge_ports(md, base, override)

for field in set(ALLOWED_KEYS) - set(md):
md.merge_scalar(field)
Expand All @@ -889,6 +906,23 @@ def merge_unique_items_lists(base, override):
return sorted(set().union(base, override))


def merge_ports(md, base, override):
def parse_sequence_func(seq):
acc = []
for item in seq:
acc.extend(ServicePort.parse(item))
return to_mapping(acc, 'merge_field')

field = 'ports'

if not md.needs_merge(field):
return

merged = parse_sequence_func(md.base.get(field, []))
merged.update(parse_sequence_func(md.override.get(field, [])))
md[field] = [item for item in sorted(merged.values())]


def merge_build(output, base, override):
def to_dict(service):
build_config = service.get('build', {})
Expand Down
17 changes: 15 additions & 2 deletions compose/config/config_schema_v3.1.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,21 @@
"ports": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "ports"
"oneOf": [
{"type": "number", "format": "ports"},
{"type": "string", "format": "ports"},
{
"type": "object",
"properties": {
"mode": {"type": "string"},
"target": {"type": "integer"},
"published": {"type": "integer"},
"protocol": {"type": "string"}
},
"required": ["target"],
"additionalProperties": false
}
]
},
"uniqueItems": true
},
Expand Down
14 changes: 12 additions & 2 deletions compose/config/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@
from compose.config import types
from compose.config.config import V1
from compose.config.config import V2_1
from compose.config.config import V3_1


def serialize_config_type(dumper, data):
representer = dumper.represent_str if six.PY3 else dumper.represent_unicode
return representer(data.repr())


def serialize_dict_type(dumper, data):
return dumper.represent_dict(data.repr())


yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type)
yaml.SafeDumper.add_representer(types.ServiceSecret, serialize_dict_type)
yaml.SafeDumper.add_representer(types.ServicePort, serialize_dict_type)


def denormalize_config(config):
Expand Down Expand Up @@ -102,7 +109,10 @@ def denormalize_service_dict(service_dict, version):
service_dict['healthcheck']['timeout']
)

if 'secrets' in service_dict:
service_dict['secrets'] = map(lambda s: s.repr(), service_dict['secrets'])
if 'ports' in service_dict and version != V3_1:
service_dict['ports'] = map(
lambda p: p.legacy_repr() if isinstance(p, types.ServicePort) else p,
service_dict['ports']
)

return service_dict
59 changes: 59 additions & 0 deletions compose/config/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from collections import namedtuple

import six
from docker.utils.ports import build_port_bindings

from ..const import COMPOSEFILE_V1 as V1
from .errors import ConfigurationError
Expand Down Expand Up @@ -258,3 +259,61 @@ def repr(self):
return dict(
[(k, v) for k, v in self._asdict().items() if v is not None]
)


class ServicePort(namedtuple('_ServicePort', 'target published protocol mode external_ip')):

@classmethod
def parse(cls, spec):
if not isinstance(spec, dict):
result = []
for k, v in build_port_bindings([spec]).items():
if '/' in k:
target, proto = k.split('/', 1)
else:
target, proto = (k, None)
for pub in v:
if pub is None:
result.append(
cls(target, None, proto, None, None)
)
elif isinstance(pub, tuple):
result.append(
cls(target, pub[1], proto, None, pub[0])
)
else:
result.append(
cls(target, pub, proto, None, None)
)
return result

return [cls(
spec.get('target'),
spec.get('published'),
spec.get('protocol'),
spec.get('mode'),
None
)]

@property
def merge_field(self):
return (self.target, self.published)

def repr(self):
return dict(
[(k, v) for k, v in self._asdict().items() if v is not None]
)

def legacy_repr(self):
return normalize_port_dict(self.repr())


def normalize_port_dict(port):
return '{external_ip}{has_ext_ip}{published}{is_pub}{target}/{protocol}'.format(
published=port.get('published', ''),
is_pub=(':' if port.get('published') else ''),
target=port.get('target'),
protocol=port.get('protocol', 'tcp'),
external_ip=port.get('external_ip', ''),
has_ext_ip=(':' if port.get('external_ip') else ''),
)
25 changes: 20 additions & 5 deletions compose/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from . import progress_stream
from .config import DOCKER_CONFIG_KEYS
from .config import merge_environment
from .config.types import ServicePort
from .config.types import VolumeSpec
from .const import DEFAULT_TIMEOUT
from .const import IS_WINDOWS_PLATFORM
Expand Down Expand Up @@ -693,7 +694,7 @@ def _get_container_create_options(

if 'ports' in container_options or 'expose' in self.options:
container_options['ports'] = build_container_ports(
container_options,
formatted_ports(container_options.get('ports', [])),
self.options)

container_options['environment'] = merge_environment(
Expand Down Expand Up @@ -747,7 +748,9 @@ def _get_container_host_config(self, override_options, one_off=False):

host_config = self.client.create_host_config(
links=self._get_links(link_to_self=one_off),
port_bindings=build_port_bindings(options.get('ports') or []),
port_bindings=build_port_bindings(
formatted_ports(options.get('ports', []))
),
binds=options.get('binds'),
volumes_from=self._get_volumes_from(),
privileged=options.get('privileged', False),
Expand Down Expand Up @@ -875,7 +878,10 @@ def remove_image(self, image_type):

def specifies_host_port(self):
def has_host_port(binding):
_, external_bindings = split_port(binding)
if isinstance(binding, dict):
external_bindings = binding.get('published')
else:
_, external_bindings = split_port(binding)

# there are no external bindings
if external_bindings is None:
Expand Down Expand Up @@ -1214,12 +1220,21 @@ def format_env(key, value):
return '{key}={value}'.format(key=key, value=value)
return [format_env(*item) for item in environment.items()]


# Ports
def formatted_ports(ports):
result = []
for port in ports:
if isinstance(port, ServicePort):
result.append(port.legacy_repr())
else:
result.append(port)
return result


def build_container_ports(container_options, options):
def build_container_ports(container_ports, options):
ports = []
all_ports = container_options.get('ports', []) + options.get('expose', [])
all_ports = container_ports + options.get('expose', [])
for port_range in all_ports:
internal_range, _ = split_port(port_range)
for port in internal_range:
Expand Down
13 changes: 13 additions & 0 deletions tests/acceptance/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1759,6 +1759,19 @@ def get_port(number):
self.assertEqual(get_port(3001), "0.0.0.0:49152")
self.assertEqual(get_port(3002), "0.0.0.0:49153")

def test_expanded_port(self):
self.base_dir = 'tests/fixtures/ports-composefile'
self.dispatch(['-f', 'expanded-notation.yml', 'up', '-d'])
container = self.project.get_service('simple').get_container()

def get_port(number):
result = self.dispatch(['port', 'simple', str(number)])
return result.stdout.rstrip()

self.assertEqual(get_port(3000), container.get_local_port(3000))
self.assertEqual(get_port(3001), "0.0.0.0:49152")
self.assertEqual(get_port(3002), "0.0.0.0:49153")

def test_port_with_scale(self):
self.base_dir = 'tests/fixtures/ports-composefile-scale'
self.dispatch(['scale', 'simple=2'], None)
Expand Down
15 changes: 15 additions & 0 deletions tests/fixtures/ports-composefile/expanded-notation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: '3.1'
services:
simple:
image: busybox:latest
command: top
ports:
- target: 3000
- target: 3001
published: 49152
- target: 3002
published: 49153
protocol: tcp
- target: 3003
published: 49154
protocol: udp
Loading