1- require "java-properties"
2-
31module Testcontainers
42 # The DockerContainer class is used to manage Docker containers.
53 # It provides an interface to create, start, stop, and manipulate containers
@@ -24,7 +22,7 @@ class DockerContainer
2422 attr_accessor :name , :image , :command , :entrypoint , :exposed_ports , :port_bindings , :volumes , :filesystem_binds ,
2523 :env , :labels , :working_dir , :healthcheck , :wait_for
2624 attr_accessor :logger
27- attr_reader :_container , :_id
25+ attr_reader :_container , :_id , :networks
2826
2927 # Initializes a new DockerContainer instance.
3028 #
@@ -62,6 +60,8 @@ def initialize(image, name: nil, command: nil, entrypoint: nil, exposed_ports: n
6260 @_container = nil
6361 @_id = nil
6462 @_created_at = nil
63+ @networks = { }
64+ @pending_network_aliases = [ ]
6565 end
6666
6767 # Add environment variables to the container configuration.
@@ -458,6 +458,34 @@ def with_wait_for(method = nil, *args, **kwargs, &block)
458458 self
459459 end
460460
461+ # Attach the container to a Docker network.
462+ #
463+ # @param network [String, Docker::Network, Testcontainers::Network] The network to attach to.
464+ # @param aliases [Array<String>, nil] Optional aliases for the container on this network.
465+ # @return [DockerContainer] The updated DockerContainer instance.
466+ def with_network ( network , aliases : nil )
467+ add_network ( network , aliases : aliases )
468+ self
469+ end
470+
471+ # Attach the container to multiple Docker networks.
472+ #
473+ # @param networks [Array<String, Docker::Network, Testcontainers::Network>] Networks to attach.
474+ # @return [DockerContainer] The updated DockerContainer instance.
475+ def with_networks ( *networks )
476+ networks . flatten . compact . each { |net | add_network ( net ) }
477+ self
478+ end
479+
480+ # Assign aliases for the container on its primary network.
481+ #
482+ # @param aliases [Array<String>] Aliases to add.
483+ # @return [DockerContainer] The updated DockerContainer instance.
484+ def with_network_aliases ( *aliases )
485+ add_network_aliases ( aliases )
486+ self
487+ end
488+
461489 # Starts the container, yields the container instance to the block, and stops the container.
462490 #
463491 # @yield [DockerContainer] The container instance.
@@ -475,17 +503,7 @@ def use
475503 # @raise [ConnectionError] If the connection to the Docker daemon fails.
476504 # @raise [NotFoundError] If Docker is unable to find the image.
477505 def start
478- expanded_path = File . expand_path ( "~/.testcontainers.properties" )
479-
480- properties = File . exist? ( expanded_path ) ? JavaProperties . load ( expanded_path ) : { }
481-
482- tc_host = ENV [ "TESTCONTAINERS_HOST" ] || properties [ :"tc.host" ]
483-
484- if tc_host && !tc_host . empty?
485- Docker . url = tc_host
486- end
487-
488- connection = Docker ::Connection . new ( Docker . url , Docker . options )
506+ connection = Testcontainers ::DockerClient . connection
489507
490508 image_options = { "fromImage" => @image } . merge ( @image_create_options )
491509 image_reference = ( image_options [ "fromImage" ] || image_options [ :fromImage ] || @image ) . to_s
@@ -500,7 +518,9 @@ def start
500518 Docker ::Image . create ( image_options , connection )
501519 end
502520
503- @_container ||= Docker ::Container . create ( _container_create_options )
521+ ensure_networks_created
522+
523+ @_container ||= Docker ::Container . create ( _container_create_options , connection )
504524 @_container . start
505525
506526 @_id = @_container . id
@@ -1101,11 +1121,13 @@ def process_env_input(env_or_key, value = nil)
11011121 end
11021122
11031123 def container_bridge_ip
1104- @_container &.json &.dig ( "NetworkSettings" , "Networks" , "bridge" , "IPAddress" )
1124+ network_key = primary_network_name || "bridge"
1125+ @_container &.json &.dig ( "NetworkSettings" , "Networks" , network_key , "IPAddress" )
11051126 end
11061127
11071128 def container_gateway_ip
1108- @_container &.json &.dig ( "NetworkSettings" , "Networks" , "bridge" , "Gateway" )
1129+ network_key = primary_network_name || "bridge"
1130+ @_container &.json &.dig ( "NetworkSettings" , "Networks" , network_key , "Gateway" )
11091131 end
11101132
11111133 def container_port ( port )
@@ -1157,11 +1179,109 @@ def _container_create_options
11571179 "Labels" => @labels ,
11581180 "WorkingDir" => @working_dir ,
11591181 "Healthcheck" => @healthcheck ,
1160- "HostConfig" => {
1161- "PortBindings" => @port_bindings ,
1162- "Binds" => @filesystem_binds
1163- } . compact
1164- } . compact
1182+ "HostConfig" => host_config_options . compact
1183+ } . compact . tap do |options |
1184+ networking = networking_config
1185+ options [ "NetworkingConfig" ] = networking if networking
1186+ end
1187+ end
1188+
1189+ def host_config_options
1190+ host_config = {
1191+ "PortBindings" => @port_bindings ,
1192+ "Binds" => @filesystem_binds
1193+ }
1194+
1195+ primary = primary_network_name
1196+ host_config [ "NetworkMode" ] = primary if primary
1197+
1198+ host_config
1199+ end
1200+
1201+ def networking_config
1202+ return if @networks . nil? || @networks . empty?
1203+
1204+ endpoints = { }
1205+ @networks . each do |name , config |
1206+ endpoint = { }
1207+ aliases = config [ :aliases ]
1208+ endpoint [ "Aliases" ] = aliases if aliases && !aliases . empty?
1209+ endpoints [ name ] = endpoint
1210+ end
1211+
1212+ return if endpoints . empty?
1213+
1214+ { "EndpointsConfig" => endpoints }
1215+ end
1216+
1217+ def primary_network_name
1218+ return nil if @networks . nil? || @networks . empty?
1219+
1220+ @networks . keys . first
1221+ end
1222+
1223+ def add_network ( network , aliases : nil )
1224+ name , network_object = resolve_network ( network )
1225+ @networks [ name ] ||= { aliases : [ ] , object : network_object }
1226+ @networks [ name ] [ :object ] ||= network_object if network_object
1227+
1228+ new_aliases = normalize_aliases ( aliases )
1229+ unless new_aliases . empty?
1230+ @networks [ name ] [ :aliases ] = ( @networks [ name ] [ :aliases ] + new_aliases ) . uniq
1231+ end
1232+
1233+ if @networks . length == 1 && @pending_network_aliases . any?
1234+ @networks [ name ] [ :aliases ] = ( @networks [ name ] [ :aliases ] + @pending_network_aliases ) . uniq
1235+ @pending_network_aliases . clear
1236+ end
1237+
1238+ self
1239+ end
1240+
1241+ def add_network_aliases ( aliases )
1242+ normalized = normalize_aliases ( aliases )
1243+ return if normalized . empty?
1244+
1245+ if @networks . nil? || @networks . empty?
1246+ @pending_network_aliases = ( @pending_network_aliases + normalized ) . uniq
1247+ else
1248+ primary = primary_network_name
1249+ @networks [ primary ] [ :aliases ] = ( @networks [ primary ] [ :aliases ] + normalized ) . uniq
1250+ end
1251+ end
1252+
1253+ def normalize_aliases ( aliases )
1254+ Array ( aliases ) . flatten . compact . filter_map do |alias_value |
1255+ value = alias_value . to_s . strip
1256+ value unless value . empty?
1257+ end . uniq
1258+ end
1259+
1260+ def resolve_network ( network )
1261+ case network
1262+ when Testcontainers ::Network
1263+ network . create
1264+ [ network . name , network ]
1265+ when Docker ::Network
1266+ info = network . info || { }
1267+ name = info [ "Name" ] || network . id
1268+ [ name , network ]
1269+ when String
1270+ [ network , nil ]
1271+ else
1272+ raise ArgumentError , "Unsupported network type: #{ network . inspect } "
1273+ end
1274+ end
1275+
1276+ def ensure_networks_created
1277+ return if @networks . nil? || @networks . empty?
1278+
1279+ @networks . each_value do |config |
1280+ network_object = config [ :object ]
1281+ next unless network_object
1282+
1283+ network_object . create if network_object . respond_to? ( :create )
1284+ end
11651285 end
11661286 end
11671287
0 commit comments