Skip to content

Commit afac587

Browse files
committed
Add support for .with_network and .with_network_aliases
1 parent 4341ff5 commit afac587

File tree

7 files changed

+355
-57
lines changed

7 files changed

+355
-57
lines changed

core/Gemfile.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ GEM
99
remote: https://rubygems.org/
1010
specs:
1111
ast (2.4.2)
12+
base64 (0.2.0)
1213
docker-api (2.2.0)
1314
excon (>= 0.47.0)
1415
multi_json
@@ -21,6 +22,7 @@ GEM
2122
minitest-hooks (1.5.0)
2223
minitest (> 5.3)
2324
multi_json (1.15.0)
25+
mutex_m (0.3.0)
2426
parallel (1.23.0)
2527
parser (3.2.2.1)
2628
ast (~> 2.4.1)
@@ -63,8 +65,10 @@ PLATFORMS
6365
x86_64-linux
6466

6567
DEPENDENCIES
68+
base64 (~> 0.2.0)
6669
minitest (~> 5.0)
6770
minitest-hooks (~> 1.5)
71+
mutex_m (~> 0.3.0)
6872
rake (~> 13.0)
6973
standard (~> 1.3)
7074
testcontainers-core!

core/lib/testcontainers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require "open3"
66
require "uri"
77
require "testcontainers/docker_container"
8+
require "testcontainers/network"
89
require_relative "testcontainers/version"
910

1011
module Testcontainers

core/lib/testcontainers/docker_container.rb

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require "java-properties"
22

33
module Testcontainers
4+
45
# The DockerContainer class is used to manage Docker containers.
56
# It provides an interface to create, start, stop, and manipulate containers
67
# using the Docker API.
@@ -21,8 +22,22 @@ module Testcontainers
2122
# @attr_reader _container [Docker::Container, nil] the underlying Docker::Container object
2223
# @attr_reader _id [String, nil] the container's ID
2324
class DockerContainer
25+
class << self
26+
def setup_docker
27+
expanded_path = File.expand_path("~/.testcontainers.properties")
28+
29+
properties = File.exist?(expanded_path) ? JavaProperties.load(expanded_path) : {}
30+
31+
tc_host = ENV["TESTCONTAINERS_HOST"] || properties[:"tc.host"]
32+
33+
if tc_host && !tc_host.empty?
34+
Docker.url = tc_host
35+
end
36+
end
37+
end
38+
2439
attr_accessor :name, :image, :command, :entrypoint, :exposed_ports, :port_bindings, :volumes, :filesystem_binds,
25-
:env, :labels, :working_dir, :healthcheck, :wait_for
40+
:env, :labels, :working_dir, :healthcheck, :wait_for, :aliases, :network
2641
attr_accessor :logger
2742
attr_reader :_container, :_id
2843

@@ -39,9 +54,11 @@ class DockerContainer
3954
# @param env [Array<String>, Hash, nil] an array or a hash of environment variables for the container in the format KEY=VALUE
4055
# @param labels [Hash, nil] a hash of labels to be applied to the container
4156
# @param working_dir [String, nil] the working directory for the container
57+
# @param network [Testcontainers::Network, nil] the network to attach the container to
58+
# @param aliases [Array<String>, nil] the aliases for the container in the network
4259
# @param logger [Logger] a logger instance for the container
4360
def initialize(image, name: nil, command: nil, entrypoint: nil, exposed_ports: nil, image_create_options: {}, port_bindings: nil, volumes: nil, filesystem_binds: nil,
44-
env: nil, labels: nil, working_dir: nil, healthcheck: nil, wait_for: nil, logger: Testcontainers.logger)
61+
env: nil, labels: nil, working_dir: nil, healthcheck: nil, wait_for: nil, network: nil, aliases: nil, logger: Testcontainers.logger)
4562

4663
@image = image
4764
@name = name
@@ -61,6 +78,8 @@ def initialize(image, name: nil, command: nil, entrypoint: nil, exposed_ports: n
6178
@_container = nil
6279
@_id = nil
6380
@_created_at = nil
81+
@aliases = aliases
82+
@network = network
6483
end
6584

6685
# Add environment variables to the container configuration.
@@ -87,7 +106,7 @@ def add_exposed_port(port)
87106
@exposed_ports ||= {}
88107
@port_bindings ||= {}
89108
@exposed_ports[port] ||= {}
90-
@port_bindings[port] ||= [{"HostPort" => ""}]
109+
@port_bindings[port] ||= [{ "HostPort" => "" }]
91110
@exposed_ports
92111
end
93112

@@ -119,7 +138,7 @@ def add_fixed_exposed_port(container_port, host_port = nil)
119138
@exposed_ports ||= {}
120139
@port_bindings ||= {}
121140
@exposed_ports[container_port] = {}
122-
@port_bindings[container_port] = [{"HostPort" => host_port.to_s}]
141+
@port_bindings[container_port] = [{ "HostPort" => host_port.to_s }]
123142
@port_bindings
124143
end
125144

@@ -229,7 +248,7 @@ def add_healthcheck(options = {})
229248
test = options[:test]
230249

231250
if test.nil?
232-
@healthcheck = {"Test" => ["NONE"]}
251+
@healthcheck = { "Test" => ["NONE"] }
233252
return @healthcheck
234253
end
235254

@@ -457,6 +476,34 @@ def with_wait_for(method = nil, *args, **kwargs, &block)
457476
self
458477
end
459478

479+
# Returns the container's ID.
480+
#
481+
def id
482+
@_id
483+
end
484+
485+
# Returns the container's aliases within the network.
486+
#
487+
def aliases
488+
@aliases ||= []
489+
end
490+
491+
# Sets the container's network.
492+
#
493+
# @param network [Testcontainers::Network] The network to attach the container to.
494+
def with_network(network)
495+
@network = network
496+
self
497+
end
498+
499+
# Sets the container's network aliases.
500+
#
501+
# @param aliases [Array<String>] The aliases for the container in the network.
502+
def with_network_aliases(*aliases)
503+
self.aliases += aliases&.flatten
504+
self
505+
end
506+
460507
# Starts the container, yields the container instance to the block, and stops the container.
461508
#
462509
# @yield [DockerContainer] The container instance.
@@ -474,19 +521,13 @@ def use
474521
# @raise [ConnectionError] If the connection to the Docker daemon fails.
475522
# @raise [NotFoundError] If Docker is unable to find the image.
476523
def start
477-
expanded_path = File.expand_path("~/.testcontainers.properties")
478-
479-
properties = File.exist?(expanded_path) ? JavaProperties.load(expanded_path) : {}
480-
481-
tc_host = ENV["TESTCONTAINERS_HOST"] || properties[:"tc.host"]
482-
483-
if tc_host && !tc_host.empty?
484-
Docker.url = tc_host
485-
end
524+
self.class.setup_docker
486525

487526
connection = Docker::Connection.new(Docker.url, Docker.options)
488527

489-
Docker::Image.create({"fromImage" => @image}.merge(@image_create_options), connection)
528+
@network&.create
529+
530+
Docker::Image.create({ "fromImage" => @image }.merge(@image_create_options), connection)
490531

491532
@_container ||= Docker::Container.create(_container_create_options)
492533
@_container.start
@@ -516,6 +557,7 @@ def start
516557
def stop(force: false)
517558
raise ContainerNotStartedError unless @_container
518559
@_container.stop(force: force)
560+
@network&.close
519561
self
520562
rescue Excon::Error::Socket => e
521563
raise ConnectionError, e.message
@@ -1026,7 +1068,7 @@ def normalize_port_bindings(port_bindings)
10261068
return port_bindings if port_bindings.is_a?(Hash) && port_bindings.values.all? { |v| v.is_a?(Array) }
10271069

10281070
port_bindings.each_with_object({}) do |(container_port, host_port), hash|
1029-
hash[normalize_port(container_port)] = [{"HostPort" => host_port.to_s}]
1071+
hash[normalize_port(container_port)] = [{ "HostPort" => host_port.to_s }]
10301072
end
10311073
end
10321074

@@ -1126,6 +1168,10 @@ def docker_host
11261168
nil
11271169
end
11281170

1171+
def network_name
1172+
@network_name ||= network&.name
1173+
end
1174+
11291175
def _container_create_options
11301176
{
11311177
"name" => @name,
@@ -1139,11 +1185,24 @@ def _container_create_options
11391185
"WorkingDir" => @working_dir,
11401186
"Healthcheck" => @healthcheck,
11411187
"HostConfig" => {
1188+
"NetworkMode" => network_name,
11421189
"PortBindings" => @port_bindings,
11431190
"Binds" => @filesystem_binds
1144-
}.compact
1191+
}.compact,
1192+
"NetworkingConfig": _networking_config
11451193
}.compact
11461194
end
1195+
1196+
def _networking_config
1197+
return nil unless network_name && !aliases&.empty?
1198+
{
1199+
"EndpointsConfig" => {
1200+
network_name => {
1201+
"Aliases" => aliases
1202+
}
1203+
}
1204+
}
1205+
end
11471206
end
11481207

11491208
# Alias for forward-compatibility

core/lib/testcontainers/network.rb

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
module Testcontainers
2+
class Network
3+
class << self
4+
def new_network(name: nil, driver: "bridge", options: {})
5+
new(name: name, driver: driver, options: options)
6+
end
7+
end
8+
9+
attr_reader :name, :driver, :options
10+
11+
def initialize(name: nil, driver: "bridge", options: {})
12+
@name = name || SecureRandom.uuid
13+
@driver = driver
14+
@options = options
15+
@network = nil
16+
end
17+
18+
def create(conn = Docker.connection)
19+
return network if created?
20+
21+
::Testcontainers::DockerContainer.setup_docker
22+
23+
@network = Docker::Network.create name, options, conn
24+
@created = true
25+
network
26+
end
27+
28+
def created?
29+
@created
30+
end
31+
32+
def close
33+
_close
34+
end
35+
36+
def info
37+
network&.json
38+
end
39+
40+
private
41+
42+
def network
43+
@network
44+
end
45+
46+
def _close
47+
return unless created?
48+
49+
begin
50+
network.remove(force: true)
51+
@created = false
52+
rescue Docker::Error::NotFoundError
53+
# Network already removed
54+
end
55+
end
56+
57+
SHARED = Testcontainers::Network.new_network
58+
59+
def SHARED.close
60+
# prevent closing the shared network
61+
end
62+
63+
# Should be called when the process exits
64+
def SHARED.force_close
65+
_close
66+
end
67+
68+
at_exit do
69+
SHARED.force_close
70+
end
71+
end
72+
end

0 commit comments

Comments
 (0)