77import threading
88import time
99from importlib import metadata
10+ from typing import Mapping
1011
1112# Packages
1213import docker
14+ import docker .errors
15+ import docker .types
16+ import docker .models .containers
1317import dockerpty
1418from dotenv import dotenv_values
1519from slugify import slugify
@@ -24,6 +28,8 @@ def __init__(self):
2428 self .cwd = os .getcwd ()
2529 self .project_name = slugify (os .path .basename (self .cwd ))
2630 self .project_port = dotenv_values (".env" ).get ("PORT" , 8080 )
31+ if self .project_port is not None :
32+ self .project_port = int (self .project_port )
2733 self .container_home = "/home/ubuntu/"
2834 self .container_path = f"{ self .container_home } { self .project_name } "
2935 # --network host is only supported on Linux
@@ -132,11 +138,11 @@ def _prepare_mounts(self, command):
132138
133139 additional_mounts = self ._get_additional_mounts (command )
134140 if additional_mounts :
135- for mount in additional_mounts :
141+ for host_path , container_mount in additional_mounts . items () :
136142 mounts .append (
137143 docker .types .Mount (
138- target = f"{ self .container_path } /{ mount [ 1 ] } " ,
139- source = f"{ mount [ 0 ] } " ,
144+ target = f"{ self .container_home } /{ container_mount } " ,
145+ source = f"{ host_path } " ,
140146 type = "bind" ,
141147 read_only = False ,
142148 consistency = "cached" ,
@@ -164,35 +170,56 @@ def _get_container_name(self, command=None):
164170 # Remove duplicated hyphens
165171 return re .sub (r"(-)+" , r"\1" , name )
166172
167- def _get_additional_mounts (self , command ):
168- """
169- Return a list of additional mounts
170- """
171- if "-m" not in command :
172- return
173+ def _get_binding_attrs (self , option , command ) -> Mapping [str , str ]:
174+ if option not in command :
175+ return {}
173176
174- def get_mount (command , mounts ):
175- mount_index = command .index ("-m" )
176- mount_string = command [mount_index + 1 ]
177- del command [mount_index ]
178- if ":" in mount_string :
179- mount_parts = mount_string .split (":" )
180- mounts . append ( mount_parts )
181- del command [mount_index ]
177+ def get_attributes (command , attributes ):
178+ index : int = command .index (option )
179+ option_value : str = command [index + 1 ]
180+ del command [index ]
181+ if ":" in option_value :
182+ binding_parts = option_value .split (":" )
183+ attributes [ binding_parts [ 0 ]] = binding_parts [ 1 ]
184+ del command [index ]
182185
183- if "-m" in command :
184- mounts = get_mount (command , mounts )
186+ # check for extra options with the same value,
187+ # for example multiple mounts or ports
188+ if option in command :
189+ attributes = get_attributes (command , attributes )
185190
186- return mounts
191+ return attributes
187192
188- return get_mount (command , [] )
193+ return get_attributes (command , {} )
189194
190- def create_container (self , command , image_name = None ):
195+ def _get_additional_ports (self , command ) -> Mapping [str , str ]:
196+ """
197+ Return a list of additional ports to expose in the container
198+ """
199+ return self ._get_binding_attrs ("-p" , command )
200+
201+ def _get_additional_mounts (self , command ):
202+ """
203+ Return a list of additional mounts
204+ """
205+ return self ._get_binding_attrs ("-m" , command )
206+
207+ def create_container (
208+ self , command , image_name = None
209+ ) -> docker .models .containers .Container :
191210 if not image_name :
192211 image_name = self .BASE_IMAGE_NAME
193- ports = {self .project_port : self .project_port }
212+
213+ # set up binding ports (container:host)
214+ ports = {}
215+ ports [str (self .project_port )] = self .project_port
216+ additional_ports = self ._get_additional_ports (command )
217+ for container_port , host_port in additional_ports .items ():
218+ ports [container_port ] = int (host_port )
219+
194220 # Run on the same network mode as the host
195221 network_mode = None
222+
196223 if command [1 :]:
197224 first_cmd = command [1 :][0 ]
198225
@@ -204,6 +231,7 @@ def create_container(self, command, image_name=None):
204231 name = self ._get_container_name (first_cmd )
205232 else :
206233 name = self ._get_container_name ()
234+
207235 if self .network_host_mode :
208236 # network_mode host is incompatible with ports option
209237 ports = None
@@ -221,7 +249,7 @@ def create_container(self, command, image_name=None):
221249 command = command ,
222250 ports = ports ,
223251 network_mode = network_mode ,
224- )
252+ ) # type: ignore
225253
226254
227255def _extract_cli_command_arg (pattern , command_list ):
0 commit comments