Repository: docker/docker-py

Issue:
TTY support on Docker services
The Docker REST API supports a TTY parameter on /service/create, but the Python API does not. See [here](https://docs.docker.com/engine/api/v1.29/#operation/ServiceCreate). This was implemented in Moby [here](https://github.com/moby/moby/pull/28076).


Discussion:

Resolving patch:
diff --git a/docker/api/service.py b/docker/api/service.py
--- a/docker/api/service.py
+++ b/docker/api/service.py
@@ -38,6 +38,11 @@ def _check_api_features(version, task_template, update_config):
                         'Placement.preferences is not supported in'
                         ' API version < 1.27'
                     )
+        if task_template.container_spec.get('TTY'):
+            if utils.version_lt(version, '1.25'):
+                raise errors.InvalidVersion(
+                    'ContainerSpec.TTY is not supported in API version < 1.25'
+                )
 
 
 class ServiceApiMixin(object):
diff --git a/docker/models/services.py b/docker/models/services.py
--- a/docker/models/services.py
+++ b/docker/models/services.py
@@ -146,6 +146,7 @@ def create(self, image, command=None, **kwargs):
                 of the service. Default: ``None``
             user (str): User to run commands as.
             workdir (str): Working directory for commands to run.
+            tty (boolean): Whether a pseudo-TTY should be allocated.
 
         Returns:
             (:py:class:`Service`) The created service.
@@ -212,6 +213,7 @@ def list(self, **kwargs):
     'mounts',
     'stop_grace_period',
     'secrets',
+    'tty'
 ]
 
 # kwargs to copy straight over to TaskTemplate
diff --git a/docker/types/services.py b/docker/types/services.py
--- a/docker/types/services.py
+++ b/docker/types/services.py
@@ -84,10 +84,11 @@ class ContainerSpec(dict):
             terminate before forcefully killing it.
         secrets (list of py:class:`SecretReference`): List of secrets to be
             made available inside the containers.
+        tty (boolean): Whether a pseudo-TTY should be allocated.
     """
     def __init__(self, image, command=None, args=None, hostname=None, env=None,
                  workdir=None, user=None, labels=None, mounts=None,
-                 stop_grace_period=None, secrets=None):
+                 stop_grace_period=None, secrets=None, tty=None):
         self['Image'] = image
 
         if isinstance(command, six.string_types):
@@ -125,6 +126,9 @@ def __init__(self, image, command=None, args=None, hostname=None, env=None,
                 raise TypeError('secrets must be a list')
             self['Secrets'] = secrets
 
+        if tty is not None:
+            self['TTY'] = tty
+
 
 class Mount(dict):
     """


Test patch:
diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py
--- a/tests/integration/api_service_test.py
+++ b/tests/integration/api_service_test.py
@@ -359,6 +359,23 @@ def test_create_service_with_env(self):
         assert 'Env' in con_spec
         assert con_spec['Env'] == ['DOCKER_PY_TEST=1']
 
+    @requires_api_version('1.25')
+    def test_create_service_with_tty(self):
+        container_spec = docker.types.ContainerSpec(
+            BUSYBOX, ['true'], tty=True
+        )
+        task_tmpl = docker.types.TaskTemplate(
+            container_spec,
+        )
+        name = self.get_service_name()
+        svc_id = self.client.create_service(task_tmpl, name=name)
+        svc_info = self.client.inspect_service(svc_id)
+        assert 'TaskTemplate' in svc_info['Spec']
+        assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate']
+        con_spec = svc_info['Spec']['TaskTemplate']['ContainerSpec']
+        assert 'TTY' in con_spec
+        assert con_spec['TTY'] is True
+
     def test_create_service_global_mode(self):
         container_spec = docker.types.ContainerSpec(
             BUSYBOX, ['echo', 'hello']