@@ -229,7 +229,17 @@ def _allocate_port(self, required_port=0, allow_dynamic=False, sleep_time=5):
229229
230230 def _setup_resource_limits_and_ports (self ):
231231 """
232- Sets up resource limits for the container based on the configuration.
232+ Sets up resource limits and port mappings for the container based on configuration.
233+
234+ Port Handling Logic:
235+ 1. Process all ports from CONTAINER_RESOURCES["ports"] first
236+ 2. If main PORT exists and not in ports mapping, allocate it
237+ 3. All ports (including main PORT) go into extra_ports_mapping
238+ 4. Validate no duplicate container ports
239+
240+ Priority:
241+ - Explicit mappings in CONTAINER_RESOURCES["ports"] take precedence
242+ - Main PORT is allocated dynamically if not explicitly mapped
233243 """
234244 DEFAULT_CPU_LIMIT = 1
235245 DEFAULT_GPU_LIMIT = 0
@@ -244,53 +254,122 @@ def _setup_resource_limits_and_ports(self):
244254
245255 ports = container_resources .get ("ports" , DEFAULT_PORTS )
246256
257+ # Track which container ports have been mapped to avoid duplicates
258+ mapped_container_ports = set ()
259+ main_port_mapped = False
260+
247261 if len (ports ) > 0 :
248262 if isinstance (ports , list ):
249- # Handle list of container ports
263+ # Handle list of container ports - allocate dynamic host ports
264+ self .P ("Processing container ports list..." )
250265 for container_port in ports :
251- self .P (f"Additional container port { container_port } specified. Finding available host port ..." )
266+ if container_port in mapped_container_ports :
267+ self .P (f"Warning: Container port { container_port } already mapped, skipping duplicate" , color = 'y' )
268+ continue
269+
270+ self .P (f"Container port { container_port } specified. Finding available host port..." )
252271 host_port = self ._allocate_port ()
253272 self .extra_ports_mapping [host_port ] = container_port
254- self .P (f"Allocated free host port { host_port } for container port { container_port } ." )
255- else :
256- # Handle dict of port mappings
257- # Check if main app port is mapped to a specific host port
258- if self .cfg_port and isinstance (ports , dict ) and self .cfg_port in ports .values ():
259- container_port = self .cfg_port
260- requested_host_port = int (next ((k for k , v in ports .items () if v == container_port ), 0 ))
261-
262- self .P (f"Main app port { self .cfg_port } is not mapped to any host port in the ports dict. Allocating a new host port ..." )
263-
264- self .port = self ._allocate_port (requested_host_port , allow_dynamic = True )
265-
266- if self .port != requested_host_port :
267- self .P (f"Requested host port { requested_host_port } is not available. Allocated port { self .port } instead." )
268-
269- self .extra_ports_mapping [self .port ] = container_port
270-
273+ mapped_container_ports .add (container_port )
274+ self .P (f"Allocated host port { host_port } -> container port { container_port } " )
275+
276+ # Check if this is the main port
277+ if self .cfg_port and container_port == self .cfg_port :
278+ self .port = host_port
279+ main_port_mapped = True
280+ self .P (f"Main PORT { self .cfg_port } mapped to host port { host_port } " , color = 'g' )
281+
282+ elif isinstance (ports , dict ):
283+ # Handle dict of explicit host_port -> container_port mappings
284+ self .P ("Processing explicit port mappings..." )
285+
286+ # First, validate for duplicate container ports
287+ container_ports_in_dict = list (ports .values ())
288+ if len (container_ports_in_dict ) != len (set (container_ports_in_dict )):
289+ raise ValueError (
290+ f"Duplicate container ports found in CONTAINER_RESOURCES['ports']: { ports } . "
291+ "Each container port can only be mapped once."
292+ )
293+
294+ # Process all explicit mappings
271295 for host_port , container_port in ports .items ():
272296 try :
273297 host_port = int (host_port )
298+ container_port = int (container_port )
299+
300+ # Check if this mapping was already processed
274301 if host_port in self .extra_ports_mapping :
275- self .Pd (f"Host port { host_port } is already allocated for container port { self .extra_ports_mapping [host_port ]} . Skipping allocation." )
276- continue
277- self ._allocate_port (host_port )
302+ existing_container_port = self .extra_ports_mapping [host_port ]
303+ if existing_container_port == container_port :
304+ self .Pd (f"Port mapping { host_port } ->{ container_port } already exists, skipping" )
305+ continue
306+ else :
307+ raise ValueError (
308+ f"Host port { host_port } is already mapped to container port { existing_container_port } . "
309+ f"Cannot map it to { container_port } "
310+ )
311+
312+ # Allocate the requested host port
313+ self .P (f"Allocating requested host port { host_port } for container port { container_port } ..." )
314+ allocated_port = self ._allocate_port (host_port , allow_dynamic = False )
315+
316+ if allocated_port != host_port :
317+ raise RuntimeError (
318+ f"Failed to allocate requested host port { host_port } . "
319+ f"Port may be in use by another process."
320+ )
321+
278322 self .extra_ports_mapping [host_port ] = container_port
323+ mapped_container_ports .add (container_port )
324+ self .P (f"Allocated host port { host_port } -> container port { container_port } " , color = 'g' )
325+
326+ # Check if this is the main port
327+ if self .cfg_port and container_port == self .cfg_port :
328+ self .port = host_port
329+ main_port_mapped = True
330+ self .P (f"Main PORT { self .cfg_port } mapped to host port { host_port } (from explicit mapping)" , color = 'g' )
331+
332+ except ValueError as e :
333+ raise ValueError (f"Invalid port mapping { host_port } :{ container_port } - { e } " )
279334 except Exception as e :
280- self .P (f"Port { host_port } is not available." )
281- self .P (e )
282- raise RuntimeError (f"Port { host_port } is not available." )
283- # endfor each port
284- # endif ports list or dict
285- # endif ports
335+ self .P (f"Failed to allocate port { host_port } : { e } " , color = 'r' )
336+ raise RuntimeError (f"Port allocation failed for { host_port } :{ container_port } " )
337+ else :
338+ self .P (f"Invalid ports configuration type: { type (ports )} . Expected list or dict." , color = 'r' )
339+
340+ # Handle main PORT if it exists and wasn't mapped yet
341+ if self .cfg_port and not main_port_mapped :
342+ if self .cfg_port in mapped_container_ports :
343+ # Main PORT was mapped to a different host port in the loop above
344+ # Find which host port it was mapped to
345+ for h_port , c_port in self .extra_ports_mapping .items ():
346+ if c_port == self .cfg_port :
347+ self .port = h_port
348+ self .P (f"Main PORT { self .cfg_port } already mapped to host port { h_port } " , color = 'd' )
349+ break
350+ else :
351+ # Allocate a dynamic host port for the main PORT
352+ self .P (f"Main PORT { self .cfg_port } not in explicit mappings. Allocating dynamic host port..." )
353+ self .port = self ._allocate_port (allow_dynamic = True )
354+ self .extra_ports_mapping [self .port ] = self .cfg_port
355+ mapped_container_ports .add (self .cfg_port )
356+ self .P (f"Allocated host port { self .port } -> main PORT { self .cfg_port } " , color = 'g' )
357+ # endif main PORT
358+ # endif main_port_mapped
286359 else :
360+ # No container resources specified, use defaults
287361 self ._cpu_limit = DEFAULT_CPU_LIMIT
288362 self ._gpu_limit = DEFAULT_GPU_LIMIT
289363 self ._mem_limit = DEFAULT_MEM_LIMIT
290- # endif resource limits
291364
292- if not self .port and self .cfg_port :
293- self .port = self ._allocate_port (allow_dynamic = True ) # Allocate a port for the container if needed
365+ # Still handle main PORT if specified
366+ if self .cfg_port :
367+ self .P (f"No CONTAINER_RESOURCES specified. Allocating dynamic host port for main PORT { self .cfg_port } ..." )
368+ self .port = self ._allocate_port (allow_dynamic = True )
369+ self .extra_ports_mapping [self .port ] = self .cfg_port
370+ self .P (f"Allocated host port { self .port } -> main PORT { self .cfg_port } " , color = 'g' )
371+ # endif main PORT
372+ # endif container_resources
294373 return
295374
296375 def _set_directory_permissions (self , path , mode = 0o777 ):
@@ -469,6 +548,12 @@ def _configure_file_volumes(self):
469548
470549 ### COMMON CONTAINER UTILITY METHODS ###
471550 def _setup_env_and_ports (self ):
551+ """
552+ Sets up environment variables and formats port mappings for Docker.
553+
554+ This method should NOT allocate ports - only format already-allocated ports.
555+ All port allocations happen in _setup_resource_limits_and_ports.
556+ """
472557 # Environment variables
473558 # allow cfg_env to override default env vars
474559 self .env = self ._get_default_env_vars ()
@@ -479,12 +564,21 @@ def _setup_env_and_ports(self):
479564 self .env .update (self .dynamic_env )
480565 # endif dynamic env
481566
482- # Ports mapping
483- ports_mapping = self .extra_ports_mapping .copy () if self .extra_ports_mapping else {}
484- if self .cfg_port and self .port :
485- ports_mapping [self .port ] = self .cfg_port
486- # end if main port
487- self .inverted_ports_mapping = {f"{ v } /tcp" : str (k ) for k , v in ports_mapping .items ()}
567+ # Format ports for Docker API
568+ # Docker expects: {"container_port/tcp": "host_port"}
569+ # extra_ports_mapping contains: {host_port: container_port}
570+ # All ports (including main PORT) are already in extra_ports_mapping
571+ self .inverted_ports_mapping = {
572+ f"{ container_port } /tcp" : str (host_port )
573+ for host_port , container_port in self .extra_ports_mapping .items ()
574+ }
575+
576+ # Log the final port mapping
577+ if self .inverted_ports_mapping :
578+ self .P ("Final port mappings:" , color = 'b' )
579+ for container_port , host_port in self .inverted_ports_mapping .items ():
580+ is_main = "(main)" if self .cfg_port and str (self .cfg_port ) in container_port else ""
581+ self .P (f" Container { container_port } -> Host { host_port } { is_main } " , color = 'd' )
488582
489583 return
490584
0 commit comments