@@ -182,6 +182,68 @@ defmodule Testcontainers.Docker.Api do
182182 end
183183 end
184184
185+ @ doc """
186+ Creates a Docker network.
187+ """
188+ def create_network ( name , conn , opts \\ [ ] ) when is_binary ( name ) do
189+ driver = Keyword . get ( opts , :driver , "bridge" )
190+
191+ body = % DockerEngineAPI.Model.NetworkCreateRequest {
192+ Name: name ,
193+ Driver: driver ,
194+ CheckDuplicate: true
195+ }
196+
197+ case Api.Network . network_create ( conn , body ) do
198+ { :ok , % DockerEngineAPI.Model.NetworkCreateResponse { Id: id } } ->
199+ { :ok , id }
200+
201+ { :ok , % DockerEngineAPI.Model.ErrorResponse { message: message } } ->
202+ { :error , { :failed_to_create_network , message } }
203+
204+ { :error , % Tesla.Env { status: 409 } } ->
205+ { :ok , :already_exists }
206+
207+ { :error , % Tesla.Env { status: status } } ->
208+ { :error , { :http_error , status } }
209+
210+ { :error , reason } ->
211+ { :error , reason }
212+ end
213+ end
214+
215+ @ doc """
216+ Removes a Docker network.
217+ """
218+ def remove_network ( name , conn ) when is_binary ( name ) do
219+ case Api.Network . network_delete ( conn , name ) do
220+ { :ok , % Tesla.Env { status: 204 } } ->
221+ :ok
222+
223+ { :ok , % Tesla.Env { status: 404 } } ->
224+ { :error , :network_not_found }
225+
226+ { :ok , % DockerEngineAPI.Model.ErrorResponse { message: message } } ->
227+ { :error , { :failed_to_remove_network , message } }
228+
229+ { :error , % Tesla.Env { status: status } } ->
230+ { :error , { :http_error , status } }
231+
232+ { :error , reason } ->
233+ { :error , reason }
234+ end
235+ end
236+
237+ @ doc """
238+ Checks if a network exists.
239+ """
240+ def network_exists? ( name , conn ) when is_binary ( name ) do
241+ case Api.Network . network_inspect ( conn , name ) do
242+ { :ok , % DockerEngineAPI.Model.Network { } } -> true
243+ _ -> false
244+ end
245+ end
246+
185247 def tag_image ( image , repo , tag , conn ) do
186248 case Api.Image . image_tag ( conn , image , repo: repo , tag: tag ) do
187249 { :ok , % Tesla.Env { status: 201 } } ->
@@ -203,21 +265,35 @@ defmodule Testcontainers.Docker.Api do
203265 end
204266
205267 defp container_create_request ( % Container { } = container_config ) do
206- % DockerEngineAPI.Model.ContainerCreateRequest {
268+ base_request = % DockerEngineAPI.Model.ContainerCreateRequest {
207269 Image: container_config . image ,
208270 Cmd: container_config . cmd ,
209271 ExposedPorts: map_exposed_ports ( container_config ) ,
210272 Env: map_env ( container_config ) ,
211273 Labels: container_config . labels ,
274+ Hostname: container_config . hostname ,
212275 HostConfig: % HostConfig {
213276 AutoRemove: container_config . auto_remove ,
214277 PortBindings: map_port_bindings ( container_config ) ,
215278 Privileged: container_config . privileged ,
216279 Binds: map_binds ( container_config ) ,
217280 Mounts: map_volumes ( container_config ) ,
218- NetworkMode: container_config . network_mode
281+ NetworkMode: container_config . network_mode || container_config . network
219282 }
220283 }
284+
285+ # Add NetworkingConfig if a network is specified
286+ if container_config . network do
287+ endpoint_config = % {
288+ container_config . network => % DockerEngineAPI.Model.EndpointSettings { }
289+ }
290+
291+ Map . put ( base_request , :NetworkingConfig , % DockerEngineAPI.Model.NetworkingConfig {
292+ EndpointsConfig: endpoint_config
293+ } )
294+ else
295+ base_request
296+ end
221297 end
222298
223299 defp map_exposed_ports ( % Container { } = container_config ) do
@@ -264,6 +340,38 @@ defmodule Testcontainers.Docker.Api do
264340 end )
265341 end
266342
343+ defp from ( % DockerEngineAPI.Model.ContainerInspectResponse {
344+ Id: container_id ,
345+ Image: image ,
346+ NetworkSettings: % { IPAddress: ip_address , Ports: ports , Networks: networks } ,
347+ Config: % { Env: env , Labels: labels }
348+ } ) do
349+ # For custom networks, the IP address is in Networks.<network_name>.IPAddress
350+ # The default bridge IPAddress will be empty for custom networks
351+ resolved_ip = resolve_ip_address ( ip_address , networks )
352+
353+ % Container {
354+ container_id: container_id ,
355+ image: image ,
356+ labels: labels ,
357+ ip_address: resolved_ip ,
358+ exposed_ports:
359+ Enum . reduce ( ports || [ ] , [ ] , fn { key , ports } , acc ->
360+ acc ++
361+ Enum . map ( ports || [ ] , fn % { "HostPort" => host_port } ->
362+ { key |> String . replace ( "/tcp" , "" ) |> String . to_integer ( ) ,
363+ host_port |> String . to_integer ( ) }
364+ end )
365+ end ) ,
366+ environment:
367+ Enum . reduce ( env || [ ] , % { } , fn env , acc ->
368+ tokens = String . split ( env , "=" )
369+ Map . merge ( acc , % { "#{ List . first ( tokens ) } ": List . last ( tokens ) } )
370+ end )
371+ }
372+ end
373+
374+ # Also handle when Networks key is missing
267375 defp from ( % DockerEngineAPI.Model.ContainerInspectResponse {
268376 Id: container_id ,
269377 Image: image ,
@@ -291,6 +399,25 @@ defmodule Testcontainers.Docker.Api do
291399 }
292400 end
293401
402+ # Resolve IP address, preferring custom network IPs if default is empty
403+ defp resolve_ip_address ( nil , networks ) , do: get_ip_from_networks ( networks )
404+ defp resolve_ip_address ( "" , networks ) , do: get_ip_from_networks ( networks )
405+ defp resolve_ip_address ( ip , _networks ) when is_binary ( ip ) and ip != "" , do: ip
406+
407+ defp get_ip_from_networks ( nil ) , do: nil
408+
409+ defp get_ip_from_networks ( networks ) when is_map ( networks ) do
410+ # Get the first non-empty IP from any network
411+ networks
412+ |> Enum . find_value ( fn
413+ { _name , % { IPAddress: ip } } when is_binary ( ip ) and ip != "" -> ip
414+ { _name , % { "IPAddress" => ip } } when is_binary ( ip ) and ip != "" -> ip
415+ _ -> nil
416+ end )
417+ end
418+
419+ defp get_ip_from_networks ( _ ) , do: nil
420+
294421 defp create_exec ( container_id , command , conn ) do
295422 data = % ExecConfig { Cmd: command }
296423
0 commit comments