-
Notifications
You must be signed in to change notification settings - Fork 27
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
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 | ||||||
|
||||||
| "NetworkingConfig": _networking_config | |
| "NetworkingConfig" => _networking_config |
Copilot
AI
Sep 3, 2025
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 condition !aliases&.empty? will return true when aliases is nil (since nil&.empty? returns nil, and !nil is true). This means networking config will be created even when there are no aliases. Use aliases&.any? instead.
| return nil unless network_name && !aliases&.empty? | |
| return nil unless network_name && aliases&.any? |
| 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&.flattenoperation will fail ifaliasesis nil because nil doesn't respond toflatten. Usealiases.flattenor handle the nil case explicitly.