-
Notifications
You must be signed in to change notification settings - Fork 22
Add support for .with_network and .with_network_aliases #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,6 +1,7 @@ | ||||||
require "java-properties" | ||||||
|
||||||
module Testcontainers | ||||||
|
||||||
# The DockerContainer class is used to manage Docker containers. | ||||||
# It provides an interface to create, start, stop, and manipulate containers | ||||||
# using the Docker API. | ||||||
|
@@ -21,8 +22,22 @@ module Testcontainers | |||||
# @attr_reader _container [Docker::Container, nil] the underlying Docker::Container object | ||||||
# @attr_reader _id [String, nil] the container's ID | ||||||
class DockerContainer | ||||||
class << self | ||||||
def setup_docker | ||||||
expanded_path = File.expand_path("~/.testcontainers.properties") | ||||||
|
||||||
properties = File.exist?(expanded_path) ? JavaProperties.load(expanded_path) : {} | ||||||
|
||||||
tc_host = ENV["TESTCONTAINERS_HOST"] || properties[:"tc.host"] | ||||||
|
||||||
if tc_host && !tc_host.empty? | ||||||
Docker.url = tc_host | ||||||
end | ||||||
end | ||||||
end | ||||||
|
||||||
attr_accessor :name, :image, :command, :entrypoint, :exposed_ports, :port_bindings, :volumes, :filesystem_binds, | ||||||
:env, :labels, :working_dir, :healthcheck, :wait_for | ||||||
:env, :labels, :working_dir, :healthcheck, :wait_for, :aliases, :network | ||||||
attr_accessor :logger | ||||||
attr_reader :_container, :_id | ||||||
|
||||||
|
@@ -39,9 +54,11 @@ class DockerContainer | |||||
# @param env [Array<String>, Hash, nil] an array or a hash of environment variables for the container in the format KEY=VALUE | ||||||
# @param labels [Hash, nil] a hash of labels to be applied to the container | ||||||
# @param working_dir [String, nil] the working directory for the container | ||||||
# @param network [Testcontainers::Network, nil] the network to attach the container to | ||||||
# @param aliases [Array<String>, nil] the aliases for the container in the network | ||||||
# @param logger [Logger] a logger instance for the container | ||||||
def initialize(image, name: nil, command: nil, entrypoint: nil, exposed_ports: nil, image_create_options: {}, port_bindings: nil, volumes: nil, filesystem_binds: nil, | ||||||
env: nil, labels: nil, working_dir: nil, healthcheck: nil, wait_for: nil, logger: Testcontainers.logger) | ||||||
env: nil, labels: nil, working_dir: nil, healthcheck: nil, wait_for: nil, network: nil, aliases: nil, logger: Testcontainers.logger) | ||||||
|
||||||
@image = image | ||||||
@name = name | ||||||
|
@@ -61,6 +78,8 @@ def initialize(image, name: nil, command: nil, entrypoint: nil, exposed_ports: n | |||||
@_container = nil | ||||||
@_id = nil | ||||||
@_created_at = nil | ||||||
@aliases = aliases | ||||||
@network = network | ||||||
end | ||||||
|
||||||
# Add environment variables to the container configuration. | ||||||
|
@@ -87,7 +106,7 @@ def add_exposed_port(port) | |||||
@exposed_ports ||= {} | ||||||
@port_bindings ||= {} | ||||||
@exposed_ports[port] ||= {} | ||||||
@port_bindings[port] ||= [{"HostPort" => ""}] | ||||||
@port_bindings[port] ||= [{ "HostPort" => "" }] | ||||||
@exposed_ports | ||||||
end | ||||||
|
||||||
|
@@ -119,7 +138,7 @@ def add_fixed_exposed_port(container_port, host_port = nil) | |||||
@exposed_ports ||= {} | ||||||
@port_bindings ||= {} | ||||||
@exposed_ports[container_port] = {} | ||||||
@port_bindings[container_port] = [{"HostPort" => host_port.to_s}] | ||||||
@port_bindings[container_port] = [{ "HostPort" => host_port.to_s }] | ||||||
@port_bindings | ||||||
end | ||||||
|
||||||
|
@@ -229,7 +248,7 @@ def add_healthcheck(options = {}) | |||||
test = options[:test] | ||||||
|
||||||
if test.nil? | ||||||
@healthcheck = {"Test" => ["NONE"]} | ||||||
@healthcheck = { "Test" => ["NONE"] } | ||||||
return @healthcheck | ||||||
end | ||||||
|
||||||
|
@@ -457,6 +476,34 @@ def with_wait_for(method = nil, *args, **kwargs, &block) | |||||
self | ||||||
end | ||||||
|
||||||
# Returns the container's ID. | ||||||
# | ||||||
def id | ||||||
@_id | ||||||
end | ||||||
|
||||||
# Returns the container's aliases within the network. | ||||||
# | ||||||
def aliases | ||||||
@aliases ||= [] | ||||||
end | ||||||
|
||||||
# Sets the container's network. | ||||||
# | ||||||
# @param network [Testcontainers::Network] The network to attach the container to. | ||||||
def with_network(network) | ||||||
@network = network | ||||||
self | ||||||
end | ||||||
|
||||||
# Sets the container's network aliases. | ||||||
# | ||||||
# @param aliases [Array<String>] The aliases for the container in the network. | ||||||
def with_network_aliases(*aliases) | ||||||
self.aliases += aliases&.flatten | ||||||
self | ||||||
end | ||||||
|
||||||
# Starts the container, yields the container instance to the block, and stops the container. | ||||||
# | ||||||
# @yield [DockerContainer] The container instance. | ||||||
|
@@ -474,19 +521,13 @@ def use | |||||
# @raise [ConnectionError] If the connection to the Docker daemon fails. | ||||||
# @raise [NotFoundError] If Docker is unable to find the image. | ||||||
def start | ||||||
expanded_path = File.expand_path("~/.testcontainers.properties") | ||||||
|
||||||
properties = File.exist?(expanded_path) ? JavaProperties.load(expanded_path) : {} | ||||||
|
||||||
tc_host = ENV["TESTCONTAINERS_HOST"] || properties[:"tc.host"] | ||||||
|
||||||
if tc_host && !tc_host.empty? | ||||||
Docker.url = tc_host | ||||||
end | ||||||
self.class.setup_docker | ||||||
|
||||||
connection = Docker::Connection.new(Docker.url, Docker.options) | ||||||
|
||||||
Docker::Image.create({"fromImage" => @image}.merge(@image_create_options), connection) | ||||||
@network&.create | ||||||
|
||||||
Docker::Image.create({ "fromImage" => @image }.merge(@image_create_options), connection) | ||||||
|
||||||
@_container ||= Docker::Container.create(_container_create_options) | ||||||
@_container.start | ||||||
|
@@ -516,6 +557,7 @@ def start | |||||
def stop(force: false) | ||||||
raise ContainerNotStartedError unless @_container | ||||||
@_container.stop(force: force) | ||||||
@network&.close | ||||||
self | ||||||
rescue Excon::Error::Socket => e | ||||||
raise ConnectionError, e.message | ||||||
|
@@ -1026,7 +1068,7 @@ def normalize_port_bindings(port_bindings) | |||||
return port_bindings if port_bindings.is_a?(Hash) && port_bindings.values.all? { |v| v.is_a?(Array) } | ||||||
|
||||||
port_bindings.each_with_object({}) do |(container_port, host_port), hash| | ||||||
hash[normalize_port(container_port)] = [{"HostPort" => host_port.to_s}] | ||||||
hash[normalize_port(container_port)] = [{ "HostPort" => host_port.to_s }] | ||||||
end | ||||||
end | ||||||
|
||||||
|
@@ -1082,11 +1124,15 @@ def process_env_input(env_or_key, value = nil) | |||||
end | ||||||
|
||||||
def container_bridge_ip | ||||||
@_container&.json&.dig("NetworkSettings", "Networks", "bridge", "IPAddress") | ||||||
network_settings&.dig("IPAddress") | ||||||
end | ||||||
|
||||||
def container_gateway_ip | ||||||
@_container&.json&.dig("NetworkSettings", "Networks", "bridge", "Gateway") | ||||||
network_settings&.dig("Gateway") | ||||||
end | ||||||
|
||||||
def network_settings | ||||||
@_container&.json&.dig("NetworkSettings", "Networks", network&.name || "bridge") | ||||||
end | ||||||
|
||||||
def container_port(port) | ||||||
|
@@ -1126,6 +1172,10 @@ def docker_host | |||||
nil | ||||||
end | ||||||
|
||||||
def network_name | ||||||
@network_name ||= network&.name | ||||||
end | ||||||
|
||||||
def _container_create_options | ||||||
{ | ||||||
"name" => @name, | ||||||
|
@@ -1139,11 +1189,24 @@ def _container_create_options | |||||
"WorkingDir" => @working_dir, | ||||||
"Healthcheck" => @healthcheck, | ||||||
"HostConfig" => { | ||||||
"NetworkMode" => network_name, | ||||||
"PortBindings" => @port_bindings, | ||||||
"Binds" => @filesystem_binds | ||||||
}.compact | ||||||
}.compact, | ||||||
"NetworkingConfig": _networking_config | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using both string keys and symbol keys in the same hash is inconsistent. Use either
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||
}.compact | ||||||
end | ||||||
|
||||||
def _networking_config | ||||||
return nil unless network_name && !aliases&.empty? | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The condition
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||
{ | ||||||
"EndpointsConfig" => { | ||||||
network_name => { | ||||||
"Aliases" => aliases | ||||||
} | ||||||
} | ||||||
} | ||||||
end | ||||||
end | ||||||
|
||||||
# Alias for forward-compatibility | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
module Testcontainers | ||
class Network | ||
class << self | ||
def new_network(name: nil, driver: "bridge", options: {}) | ||
new(name: name, driver: driver, options: options) | ||
end | ||
end | ||
|
||
attr_reader :name, :driver, :options | ||
|
||
def initialize(name: nil, driver: "bridge", options: {}) | ||
@name = name || SecureRandom.uuid | ||
@driver = driver | ||
@options = options | ||
@network = nil | ||
end | ||
|
||
def create(conn = Docker.connection) | ||
return network if created? | ||
|
||
::Testcontainers::DockerContainer.setup_docker | ||
|
||
@network = Docker::Network.create name, options, conn | ||
@created = true | ||
network | ||
end | ||
|
||
def created? | ||
@created | ||
end | ||
|
||
def close | ||
_close | ||
end | ||
|
||
def info | ||
network&.json | ||
end | ||
|
||
private | ||
|
||
def network | ||
@network | ||
end | ||
|
||
def _close | ||
return unless created? | ||
|
||
begin | ||
network.remove(force: true) | ||
@created = false | ||
rescue Docker::Error::NotFoundError | ||
# Network already removed | ||
end | ||
end | ||
|
||
SHARED = Testcontainers::Network.new_network | ||
|
||
def SHARED.close | ||
# prevent closing the shared network | ||
end | ||
|
||
# Should be called when the process exits | ||
def SHARED.force_close | ||
_close | ||
end | ||
|
||
at_exit do | ||
SHARED.force_close | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
aliases&.flatten
operation will fail ifaliases
is nil because nil doesn't respond toflatten
. Usealiases.flatten
or handle the nil case explicitly.Copilot uses AI. Check for mistakes.