Repository: docker/docker-py

Issue:
Allow exposing SCTP ports
Exposing SCTP ports has been supported in docker since 18.03 but docker-py currently does not support it but instead raises a ValueError.
Because of this compose doesn't support SCTP either, see the original issue on docker/compose (https://github.com/docker/compose/issues/6294) for more info.

I think the solution could simply be to add 'sctp' to PORT_SPEC here :
https://github.com/docker/docker-py/blob/61bab6379890841f698ed820e464bc00bf2d379d/docker/utils/ports.py#L10

Sadly I can't deliver a proper PR for this.


Discussion:
@ulyssessouza @rumpl any of you want to tackle this ?
~~@thaJeztah would it require a new version of compose schema ?~~ nvm, I thought I was triaging docker/compose :/

Resolving patch:
diff --git a/docker/api/container.py b/docker/api/container.py
--- a/docker/api/container.py
+++ b/docker/api/container.py
@@ -915,9 +915,10 @@ def port(self, container, private_port):
         if '/' in private_port:
             return port_settings.get(private_port)
 
-        h_ports = port_settings.get(private_port + '/tcp')
-        if h_ports is None:
-            h_ports = port_settings.get(private_port + '/udp')
+        for protocol in ['tcp', 'udp', 'sctp']:
+            h_ports = port_settings.get(private_port + '/' + protocol)
+            if h_ports:
+                break
 
         return h_ports
 
diff --git a/docker/models/containers.py b/docker/models/containers.py
--- a/docker/models/containers.py
+++ b/docker/models/containers.py
@@ -649,8 +649,8 @@ def run(self, image, command=None, stdout=True, stderr=False,
 
                 The keys of the dictionary are the ports to bind inside the
                 container, either as an integer or a string in the form
-                ``port/protocol``, where the protocol is either ``tcp`` or
-                ``udp``.
+                ``port/protocol``, where the protocol is either ``tcp``,
+                ``udp``, or ``sctp``.
 
                 The values of the dictionary are the corresponding ports to
                 open on the host, which can be either:
diff --git a/docker/utils/ports.py b/docker/utils/ports.py
--- a/docker/utils/ports.py
+++ b/docker/utils/ports.py
@@ -7,7 +7,7 @@
     r"(?P<ext>[\d]*)(-(?P<ext_end>[\d]+))?:"  # External range
     ")?"
     r"(?P<int>[\d]+)(-(?P<int_end>[\d]+))?"  # Internal range
-    "(?P<proto>/(udp|tcp))?"  # Protocol
+    "(?P<proto>/(udp|tcp|sctp))?"  # Protocol
     "$"  # Match full string
 )
 


Test patch:
diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py
--- a/tests/integration/api_container_test.py
+++ b/tests/integration/api_container_test.py
@@ -1083,11 +1083,17 @@ def test_port(self):
 
         port_bindings = {
             '1111': ('127.0.0.1', '4567'),
-            '2222': ('127.0.0.1', '4568')
+            '2222': ('127.0.0.1', '4568'),
+            '3333/udp': ('127.0.0.1', '4569'),
         }
+        ports = [
+            1111,
+            2222,
+            (3333, 'udp'),
+        ]
 
         container = self.client.create_container(
-            BUSYBOX, ['sleep', '60'], ports=list(port_bindings.keys()),
+            BUSYBOX, ['sleep', '60'], ports=ports,
             host_config=self.client.create_host_config(
                 port_bindings=port_bindings, network_mode='bridge'
             )
@@ -1098,13 +1104,15 @@ def test_port(self):
 
         # Call the port function on each biding and compare expected vs actual
         for port in port_bindings:
+            port, _, protocol = port.partition('/')
             actual_bindings = self.client.port(container, port)
             port_binding = actual_bindings.pop()
 
             ip, host_port = port_binding['HostIp'], port_binding['HostPort']
 
-            assert ip == port_bindings[port][0]
-            assert host_port == port_bindings[port][1]
+            port_binding = port if not protocol else port + "/" + protocol
+            assert ip == port_bindings[port_binding][0]
+            assert host_port == port_bindings[port_binding][1]
 
         self.client.kill(id)
 
diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py
--- a/tests/unit/utils_test.py
+++ b/tests/unit/utils_test.py
@@ -491,9 +491,12 @@ def test_split_port_with_host_ip(self):
         assert external_port == [("127.0.0.1", "1000")]
 
     def test_split_port_with_protocol(self):
-        internal_port, external_port = split_port("127.0.0.1:1000:2000/udp")
-        assert internal_port == ["2000/udp"]
-        assert external_port == [("127.0.0.1", "1000")]
+        for protocol in ['tcp', 'udp', 'sctp']:
+            internal_port, external_port = split_port(
+                "127.0.0.1:1000:2000/" + protocol
+            )
+            assert internal_port == ["2000/" + protocol]
+            assert external_port == [("127.0.0.1", "1000")]
 
     def test_split_port_with_host_ip_no_port(self):
         internal_port, external_port = split_port("127.0.0.1::2000")
@@ -546,6 +549,10 @@ def test_split_port_invalid(self):
         with pytest.raises(ValueError):
             split_port("0.0.0.0:1000:2000:tcp")
 
+    def test_split_port_invalid_protocol(self):
+        with pytest.raises(ValueError):
+            split_port("0.0.0.0:1000:2000/ftp")
+
     def test_non_matching_length_port_ranges(self):
         with pytest.raises(ValueError):
             split_port("0.0.0.0:1000-1010:2000-2002/tcp")