testcontainers-core 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +70 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +14 -0
- data/lib/testcontainers/docker_container.rb +878 -0
- data/lib/testcontainers/version.rb +5 -0
- data/lib/testcontainers-core.rb +1 -0
- data/lib/testcontainers.rb +30 -0
- metadata +126 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e441d19cef355259e7534abac5b40cadb36f3eba448bc278347ef4ca538d5504
|
4
|
+
data.tar.gz: ebc882a7769376f0750b3d41a38789edeca15e097a76f77bd7930ddc7c422cfe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5da561414803d9c460e90d3568f1d77567e9c0c23cdda6c81365baebded160a8be7e1edef7a48ac336ab7fcf5b9fe08316106ad5abc33311617af73f70e179a7
|
7
|
+
data.tar.gz: 74341dba25a2be881e8015b91bd206bc37948bd2bd844509553bfe60320e789684b197d1f2bc0f0008cd0342228144573aefda77b1af46ea30fc8f27ce8a62aa
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
testcontainers-core (0.1.0)
|
5
|
+
docker-api (~> 2.2)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
ast (2.4.2)
|
11
|
+
docker-api (2.2.0)
|
12
|
+
excon (>= 0.47.0)
|
13
|
+
multi_json
|
14
|
+
excon (0.99.0)
|
15
|
+
json (2.6.3)
|
16
|
+
language_server-protocol (3.17.0.3)
|
17
|
+
lint_roller (1.0.0)
|
18
|
+
minitest (5.18.0)
|
19
|
+
minitest-hooks (1.5.0)
|
20
|
+
minitest (> 5.3)
|
21
|
+
multi_json (1.15.0)
|
22
|
+
parallel (1.23.0)
|
23
|
+
parser (3.2.2.1)
|
24
|
+
ast (~> 2.4.1)
|
25
|
+
rainbow (3.1.1)
|
26
|
+
rake (13.0.6)
|
27
|
+
regexp_parser (2.8.0)
|
28
|
+
rexml (3.2.5)
|
29
|
+
rubocop (1.50.2)
|
30
|
+
json (~> 2.3)
|
31
|
+
parallel (~> 1.10)
|
32
|
+
parser (>= 3.2.0.0)
|
33
|
+
rainbow (>= 2.2.2, < 4.0)
|
34
|
+
regexp_parser (>= 1.8, < 3.0)
|
35
|
+
rexml (>= 3.2.5, < 4.0)
|
36
|
+
rubocop-ast (>= 1.28.0, < 2.0)
|
37
|
+
ruby-progressbar (~> 1.7)
|
38
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
39
|
+
rubocop-ast (1.28.0)
|
40
|
+
parser (>= 3.2.1.0)
|
41
|
+
rubocop-performance (1.16.0)
|
42
|
+
rubocop (>= 1.7.0, < 2.0)
|
43
|
+
rubocop-ast (>= 0.4.0)
|
44
|
+
ruby-progressbar (1.13.0)
|
45
|
+
standard (1.28.0)
|
46
|
+
language_server-protocol (~> 3.17.0.2)
|
47
|
+
lint_roller (~> 1.0)
|
48
|
+
rubocop (~> 1.50.2)
|
49
|
+
standard-custom (~> 1.0.0)
|
50
|
+
standard-performance (~> 1.0.1)
|
51
|
+
standard-custom (1.0.0)
|
52
|
+
lint_roller (~> 1.0)
|
53
|
+
standard-performance (1.0.1)
|
54
|
+
lint_roller (~> 1.0)
|
55
|
+
rubocop-performance (~> 1.16.0)
|
56
|
+
unicode-display_width (2.4.2)
|
57
|
+
|
58
|
+
PLATFORMS
|
59
|
+
arm64-darwin-21
|
60
|
+
x86_64-linux
|
61
|
+
|
62
|
+
DEPENDENCIES
|
63
|
+
minitest (~> 5.0)
|
64
|
+
minitest-hooks (~> 1.5)
|
65
|
+
rake (~> 13.0)
|
66
|
+
standard (~> 1.3)
|
67
|
+
testcontainers-core!
|
68
|
+
|
69
|
+
BUNDLED WITH
|
70
|
+
2.4.1
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Guillermo Iguaran
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Testcontainers
|
2
|
+
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
4
|
+
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/testcontainers`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
+
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
12
|
+
|
13
|
+
$ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
14
|
+
|
15
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
|
+
|
17
|
+
$ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Development
|
24
|
+
|
25
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
26
|
+
|
27
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
28
|
+
|
29
|
+
## Contributing
|
30
|
+
|
31
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/guilleiguaran/testcontainers. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/guilleiguaran/testcontainers/blob/main/CODE_OF_CONDUCT.md).
|
32
|
+
|
33
|
+
## License
|
34
|
+
|
35
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
36
|
+
|
37
|
+
## Code of Conduct
|
38
|
+
|
39
|
+
Everyone interacting in the Testcontainers project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/guilleiguaran/testcontainers/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
require "standard/rake"
|
13
|
+
|
14
|
+
task default: %i[test standard]
|
@@ -0,0 +1,878 @@
|
|
1
|
+
module Testcontainers
|
2
|
+
# The DockerContainer class is used to manage Docker containers.
|
3
|
+
# It provides an interface to create, start, stop, and manipulate containers
|
4
|
+
# using the Docker API.
|
5
|
+
#
|
6
|
+
# @attr name [String] the container's name
|
7
|
+
# @attr image [String] the container's image name
|
8
|
+
# @attr command [Array<String>, nil] the command to run in the container
|
9
|
+
# @attr exposed_ports [Hash, nil] a hash mapping exposed container ports to an empty hash (used for Docker API compatibility)
|
10
|
+
# @attr port_bindings [Hash, nil] a hash mapping container ports to host port bindings (used for Docker API compatibility)
|
11
|
+
# @attr volumes [Hash, nil] a hash mapping volume paths in the container to an empty hash (used for Docker API compatibility)
|
12
|
+
# @attr filesystem_binds [Array<String>, nil] an array of strings representing bind mounts from the host to the container
|
13
|
+
# @attr env [Array<String>, nil] an array of environment variables for the container in the format KEY=VALUE
|
14
|
+
# @attr labels [Hash, nil] a hash of labels to be applied to the container
|
15
|
+
# @attr working_dir [String, nil] the working directory for the container
|
16
|
+
# @attr logger [Logger] a logger instance for the container
|
17
|
+
# @attr_reader _container [Docker::Container, nil] the underlying Docker::Container object
|
18
|
+
# @attr_reader _id [String, nil] the container's ID
|
19
|
+
class DockerContainer
|
20
|
+
attr_accessor :name, :image, :command, :exposed_ports, :port_bindings, :volumes, :filesystem_binds, :env, :labels, :working_dir
|
21
|
+
attr_accessor :logger
|
22
|
+
attr_reader :_container, :_id
|
23
|
+
|
24
|
+
# Initializes a new DockerContainer instance.
|
25
|
+
#
|
26
|
+
# @param image [String] the container's image name
|
27
|
+
# @param command [Array<String>, nil] the command to run in the container
|
28
|
+
# @param name [String, nil] the container's name
|
29
|
+
# @param exposed_ports [Hash, Array<String>, nil] a hash or an array of exposed container ports
|
30
|
+
# @param port_bindings [Hash, Array<String>, nil] a hash or an array of container ports to host port bindings
|
31
|
+
# @param volumes [Hash, Array<String>, nil] a hash or an array of volume paths in the container
|
32
|
+
# @param filesystem_binds [Array<String>, Hash, nil] an array of strings or a hash representing bind mounts from the host to the container
|
33
|
+
# @param env [Array<String>, Hash, nil] an array or a hash of environment variables for the container in the format KEY=VALUE
|
34
|
+
# @param labels [Hash, nil] a hash of labels to be applied to the container
|
35
|
+
# @param working_dir [String, nil] the working directory for the container
|
36
|
+
# @param logger [Logger] a logger instance for the container
|
37
|
+
def initialize(image, command: nil, name: nil, exposed_ports: nil, port_bindings: nil, volumes: nil, filesystem_binds: nil, env: nil,
|
38
|
+
labels: nil, working_dir: nil, logger: Testcontainers.logger)
|
39
|
+
|
40
|
+
@image = image
|
41
|
+
@command = command
|
42
|
+
@name = name
|
43
|
+
@exposed_ports = add_exposed_ports(exposed_ports) if exposed_ports
|
44
|
+
@port_bindings = add_fixed_exposed_ports(port_bindings) if port_bindings
|
45
|
+
@volumes = add_volumes(volumes) if volumes
|
46
|
+
@env = add_env(env) if env
|
47
|
+
@filesystem_binds = add_filesystem_binds(filesystem_binds) if filesystem_binds
|
48
|
+
@labels = add_labels(labels) if labels
|
49
|
+
@working_dir = working_dir
|
50
|
+
@logger = logger
|
51
|
+
@_container = nil
|
52
|
+
@_id = nil
|
53
|
+
@_created_at = nil
|
54
|
+
end
|
55
|
+
|
56
|
+
# Add environment variables to the container configuration.
|
57
|
+
#
|
58
|
+
# @param env_or_key [String, Hash, Array] The environment variable(s) to add.
|
59
|
+
# - When passing a Hash, the keys and values represent the variable names and values.
|
60
|
+
# - When passing an Array, each element should be a String in the format "KEY=VALUE".
|
61
|
+
# - When passing a String, it should be in the format "KEY=VALUE" or a key when a value is also provided.
|
62
|
+
# @param value [String, nil] The value for the environment variable if env_or_key is a key (String).
|
63
|
+
# @return [Array<String>] The updated list of environment variables in the format "KEY=VALUE".
|
64
|
+
def add_env(env_or_key, value = nil)
|
65
|
+
@env ||= []
|
66
|
+
new_env = process_env_input(env_or_key, value)
|
67
|
+
@env.concat(new_env)
|
68
|
+
@env
|
69
|
+
end
|
70
|
+
|
71
|
+
# Add an exposed port to the container configuration.
|
72
|
+
#
|
73
|
+
# @param port [String, Integer] The port to expose in the format "port/protocol" or as an integer.
|
74
|
+
# @return [Hash] The updated list of exposed ports.
|
75
|
+
def add_exposed_port(port)
|
76
|
+
port = normalize_port(port)
|
77
|
+
@exposed_ports ||= {}
|
78
|
+
@port_bindings ||= {}
|
79
|
+
@exposed_ports[port] = {}
|
80
|
+
@port_bindings[port] = [{"HostPort" => ""}]
|
81
|
+
@exposed_ports
|
82
|
+
end
|
83
|
+
|
84
|
+
# Add multiple exposed ports to the container configuration.
|
85
|
+
#
|
86
|
+
# @param ports [Array<String, Integer>] The list of ports to expose.
|
87
|
+
# @return [Hash] The updated list of exposed ports
|
88
|
+
def add_exposed_ports(*ports)
|
89
|
+
ports = ports.first if ports.first.is_a?(Array)
|
90
|
+
|
91
|
+
ports.each do |port|
|
92
|
+
add_exposed_port(port)
|
93
|
+
end
|
94
|
+
@exposed_ports
|
95
|
+
end
|
96
|
+
|
97
|
+
# Add a fixed exposed port to the container configuration.
|
98
|
+
#
|
99
|
+
# @param container_port [String, Integer, Hash] The container port in the format "port/protocol" or as an integer.
|
100
|
+
# When passing a Hash, it should contain a single key-value pair with the container port as the key and the host port as the value.
|
101
|
+
# @param host_port [Integer, nil] The host port to bind the container port to.
|
102
|
+
# @return [Hash] The updated list of port bindings.
|
103
|
+
def add_fixed_exposed_port(container_port, host_port = nil)
|
104
|
+
if container_port.is_a?(Hash)
|
105
|
+
container_port, host_port = container_port.first
|
106
|
+
end
|
107
|
+
|
108
|
+
container_port = normalize_port(container_port)
|
109
|
+
@exposed_ports ||= {}
|
110
|
+
@port_bindings ||= {}
|
111
|
+
@exposed_ports[container_port] = {}
|
112
|
+
@port_bindings[container_port] = [{"HostPort" => host_port.to_s}]
|
113
|
+
@port_bindings
|
114
|
+
end
|
115
|
+
|
116
|
+
# Add multiple fixed exposed ports to the container configuration.
|
117
|
+
#
|
118
|
+
# @param port_mappings [Hash] The list of container ports and host ports to bind them to.
|
119
|
+
# @return [Hash] The updated list of port bindings.
|
120
|
+
def add_fixed_exposed_ports(port_mappings = {})
|
121
|
+
port_mappings.each do |container_port, host_port|
|
122
|
+
add_fixed_exposed_port(container_port, host_port)
|
123
|
+
end
|
124
|
+
@port_bindings
|
125
|
+
end
|
126
|
+
|
127
|
+
# Add a volume to the container configuration.
|
128
|
+
#
|
129
|
+
# @param volume [String] The volume to add.
|
130
|
+
# @return [Hash] The updated list of volumes.
|
131
|
+
def add_volume(volume)
|
132
|
+
@volumes ||= {}
|
133
|
+
@volumes[volume] = {}
|
134
|
+
@volumes
|
135
|
+
end
|
136
|
+
|
137
|
+
# Add multiple volumes to the container configuration.
|
138
|
+
#
|
139
|
+
# @param volumes [Array<String>] The list of volumes to add.
|
140
|
+
# @return [Hash] The updated list of volumes.
|
141
|
+
def add_volumes(volumes = [])
|
142
|
+
volumes = normalize_volumes(volumes)
|
143
|
+
@volumes ||= {}
|
144
|
+
@volumes.merge!(volumes)
|
145
|
+
@volumes
|
146
|
+
end
|
147
|
+
|
148
|
+
# Add a filesystem bind to the container configuration.
|
149
|
+
#
|
150
|
+
# @param host_or_hash [String, Hash] The host path or a Hash with a single key-value pair representing the host and container paths.
|
151
|
+
# @param container_path [String, nil] The container path if host_or_hash is a String.
|
152
|
+
# @param mode [String] The access mode for the bind ("rw" for read-write, "ro" for read-only). Default is "rw".
|
153
|
+
# @return [Array<String>] The updated list of filesystem binds in the format "host_path:container_path:mode".
|
154
|
+
def add_filesystem_bind(host_or_hash, container_path = nil, mode = "rw")
|
155
|
+
@filesystem_binds ||= []
|
156
|
+
|
157
|
+
if host_or_hash.is_a?(Hash)
|
158
|
+
host_path, container_path = host_or_hash.first
|
159
|
+
elsif host_or_hash.is_a?(String)
|
160
|
+
if container_path
|
161
|
+
host_path = host_or_hash
|
162
|
+
else
|
163
|
+
host_path, container_path, mode = host_or_hash.split(":")
|
164
|
+
mode ||= "rw"
|
165
|
+
end
|
166
|
+
else
|
167
|
+
raise ArgumentError, "Invalid input format for add_filesystem_bind"
|
168
|
+
end
|
169
|
+
|
170
|
+
@filesystem_binds << "#{host_path}:#{container_path}:#{mode}"
|
171
|
+
add_volume(container_path)
|
172
|
+
@filesystem_binds
|
173
|
+
end
|
174
|
+
|
175
|
+
# Add multiple filesystem binds to the container configuration.
|
176
|
+
#
|
177
|
+
# @param filesystem_binds [Array<String>, Array<Array<String>>, Hash] The list of filesystem binds.
|
178
|
+
# @return [Array<String>] The updated list of filesystem binds in the format "host_path:container_path:mode".
|
179
|
+
def add_filesystem_binds(filesystem_binds)
|
180
|
+
@filesystem_binds ||= []
|
181
|
+
binds = normalize_filesystem_binds(filesystem_binds)
|
182
|
+
binds.each do |bind|
|
183
|
+
add_filesystem_bind(*bind)
|
184
|
+
end
|
185
|
+
@filesystem_binds
|
186
|
+
end
|
187
|
+
|
188
|
+
# Add a label to the container configuration.
|
189
|
+
#
|
190
|
+
# @param label [String] The label to add.
|
191
|
+
# @param value [String] The value of the label.
|
192
|
+
# @return [Hash] The updated list of labels.
|
193
|
+
def add_label(label, value)
|
194
|
+
@labels ||= {}
|
195
|
+
@labels[label] = value
|
196
|
+
@labels
|
197
|
+
end
|
198
|
+
|
199
|
+
# Add multiple labels to the container configuration.
|
200
|
+
#
|
201
|
+
# @param labels [Hash] The labels to add.
|
202
|
+
# @return [Hash] The updated list of labels.
|
203
|
+
def add_labels(labels)
|
204
|
+
@labels ||= {}
|
205
|
+
@labels.merge!(labels)
|
206
|
+
@labels
|
207
|
+
end
|
208
|
+
|
209
|
+
# Set options for the container configuration using "with_" methods.
|
210
|
+
#
|
211
|
+
# @param options [Hash] A hash of options where keys correspond to "with_" methods and values are the arguments for those methods.
|
212
|
+
# @return [DockerContainer] The updated DockerContainer instance.
|
213
|
+
def with(options)
|
214
|
+
options.each do |key, value|
|
215
|
+
method_name = "with_#{key}"
|
216
|
+
if respond_to?(method_name)
|
217
|
+
send(method_name, value)
|
218
|
+
else
|
219
|
+
raise ArgumentError, "Invalid with_ method: #{method_name}"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
self
|
224
|
+
end
|
225
|
+
|
226
|
+
# Set the command for the container.
|
227
|
+
#
|
228
|
+
# @param parts [Array<String>] The command to run in the container as an array of strings.
|
229
|
+
# @return [DockerContainer] The updated DockerContainer instance.
|
230
|
+
def with_command(*parts)
|
231
|
+
@command = parts.first.is_a?(Array) ? parts.first : parts
|
232
|
+
|
233
|
+
self
|
234
|
+
end
|
235
|
+
|
236
|
+
# Set the name of the container.
|
237
|
+
#
|
238
|
+
# @param name [String] The name of the container.
|
239
|
+
# @return [DockerContainer] The updated DockerContainer instance.
|
240
|
+
def with_name(name)
|
241
|
+
@name = name
|
242
|
+
self
|
243
|
+
end
|
244
|
+
|
245
|
+
# Sets the container's environment variables.
|
246
|
+
#
|
247
|
+
# @param env_or_key [String, Hash, Array] The environment variable(s) to add.
|
248
|
+
# - When passing a Hash, the keys and values represent the variable names and values.
|
249
|
+
# - When passing an Array, each element should be a String in the format "KEY=VALUE".
|
250
|
+
# - When passing a String, it should be in the format "KEY=VALUE" or a key when a value is also provided.
|
251
|
+
# @param value [String, nil] The value for the environment variable if env_or_key is a key (String).
|
252
|
+
# @return [DockerContainer] The updated DockerContainer instance.
|
253
|
+
def with_env(env_or_key, value = nil)
|
254
|
+
add_env(env_or_key, value)
|
255
|
+
self
|
256
|
+
end
|
257
|
+
|
258
|
+
# Sets the container's working directory.
|
259
|
+
#
|
260
|
+
# @param working_dir [String] the working directory for the container.
|
261
|
+
# @return [DockerContainer] The updated DockerContainer instance.
|
262
|
+
def with_working_dir(working_dir)
|
263
|
+
@working_dir = working_dir
|
264
|
+
self
|
265
|
+
end
|
266
|
+
|
267
|
+
# Adds exposed ports to the container.
|
268
|
+
#
|
269
|
+
# @param ports [Array<String, Integer>] The list of ports to expose.
|
270
|
+
# @return [DockerContainer] The updated DockerContainer instance.
|
271
|
+
def with_exposed_ports(*ports)
|
272
|
+
add_exposed_ports(*ports)
|
273
|
+
self
|
274
|
+
end
|
275
|
+
|
276
|
+
# Adds a fixed exposed port to the container.
|
277
|
+
#
|
278
|
+
# @param container_port [String, Integer, Hash] The container port in the format "port/protocol" or as an integer.
|
279
|
+
# When passing a Hash, it should contain a single key-value pair with the container port as the key and the host port as the value.
|
280
|
+
# @param host_port [Integer, nil] The host port to bind the container port to.
|
281
|
+
# @return [DockerContainer] The updated DockerContainer instance.
|
282
|
+
def with_fixed_exposed_port(container_port, host_port = nil)
|
283
|
+
add_fixed_exposed_port(container_port, host_port)
|
284
|
+
self
|
285
|
+
end
|
286
|
+
|
287
|
+
# @see #with_fixed_exposed_port
|
288
|
+
alias_method :with_port_binding, :with_fixed_exposed_port
|
289
|
+
|
290
|
+
# Adds volumes to the container.
|
291
|
+
#
|
292
|
+
# @param volumes [Hash] a hash of volume key-value pairs.
|
293
|
+
# @return [DockerContainer] The updated DockerContainer instance.
|
294
|
+
def with_volumes(volumes = {})
|
295
|
+
add_volumes(volumes)
|
296
|
+
self
|
297
|
+
end
|
298
|
+
|
299
|
+
# Adds filesystem binds to the container.
|
300
|
+
# @param filesystem_binds [Array, String, Hash] an array, string, or hash of filesystem binds.
|
301
|
+
# @return [DockerContainer] The updated DockerContainer instance.
|
302
|
+
def with_filesystem_binds(filesystem_binds)
|
303
|
+
add_filesystem_binds(filesystem_binds)
|
304
|
+
self
|
305
|
+
end
|
306
|
+
|
307
|
+
# Adds labels to the container.
|
308
|
+
#
|
309
|
+
# @param labels [Hash] the labels to add.
|
310
|
+
# @return [DockerContainer] The updated DockerContainer instance.
|
311
|
+
def with_labels(labels)
|
312
|
+
add_labels(labels)
|
313
|
+
self
|
314
|
+
end
|
315
|
+
|
316
|
+
# Adds a label to the container.
|
317
|
+
#
|
318
|
+
# @param label [String] the label key.
|
319
|
+
# @param value [String] the label value.
|
320
|
+
# @return [DockerContainer] The updated DockerContainer instance.
|
321
|
+
def with_label(label, value)
|
322
|
+
add_label(label, value)
|
323
|
+
self
|
324
|
+
end
|
325
|
+
|
326
|
+
# Starts the container, yields the container instance to the block, and stops the container.
|
327
|
+
#
|
328
|
+
# @yield [DockerContainer] The container instance.
|
329
|
+
# @return [DockerContainer] Wherever the block returns.
|
330
|
+
def use
|
331
|
+
start
|
332
|
+
yield self
|
333
|
+
ensure
|
334
|
+
stop
|
335
|
+
end
|
336
|
+
|
337
|
+
# Starts the container.
|
338
|
+
#
|
339
|
+
# @return [DockerContainer] The DockerContainer instance.
|
340
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
341
|
+
def start
|
342
|
+
Docker::Image.create("fromImage" => @image)
|
343
|
+
|
344
|
+
@_container ||= Docker::Container.create(_container_create_options)
|
345
|
+
@_container.start
|
346
|
+
|
347
|
+
@_id = @_container.id
|
348
|
+
json = @_container.json
|
349
|
+
|
350
|
+
@name = json["Name"]
|
351
|
+
@_created_at = json["Created"]
|
352
|
+
|
353
|
+
self
|
354
|
+
rescue Excon::Error::Socket => e
|
355
|
+
raise ConnectionError, e.message
|
356
|
+
end
|
357
|
+
|
358
|
+
alias_method :enter, :start
|
359
|
+
|
360
|
+
# Stops the container.
|
361
|
+
#
|
362
|
+
# @param force [Boolean] Whether to force the container to stop.
|
363
|
+
# @return [DockerContainer] The DockerContainer instance.
|
364
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
365
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
366
|
+
def stop(force: false)
|
367
|
+
raise ContainerNotStartedError unless @_container
|
368
|
+
@_container.stop(force: force)
|
369
|
+
self
|
370
|
+
rescue Excon::Error::Socket => e
|
371
|
+
raise ConnectionError, e.message
|
372
|
+
end
|
373
|
+
|
374
|
+
# @see #stop
|
375
|
+
alias_method :exit, :stop
|
376
|
+
|
377
|
+
# Stops the container forcefully.
|
378
|
+
#
|
379
|
+
# @return [DockerContainer] The DockerContainer instance.
|
380
|
+
# @see #stop
|
381
|
+
def stop!
|
382
|
+
stop(force: true)
|
383
|
+
end
|
384
|
+
|
385
|
+
# Kills the container with the specified signal
|
386
|
+
#
|
387
|
+
# @param signal [String] The signal to send to the container.
|
388
|
+
# @return [DockerContainer] The DockerContainer instance.
|
389
|
+
# @return [nil] If the container does not exist.
|
390
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
391
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
392
|
+
def kill(signal: "SIGKILL")
|
393
|
+
raise ContainerNotStartedError unless @_container
|
394
|
+
@_container.kill(signal: signal)
|
395
|
+
self
|
396
|
+
rescue Excon::Error::Socket => e
|
397
|
+
raise ConnectionError, e.message
|
398
|
+
end
|
399
|
+
|
400
|
+
# Removes the container.
|
401
|
+
#
|
402
|
+
# @return [DockerContainer] The DockerContainer instance.
|
403
|
+
# @return [nil] If the container does not exist.
|
404
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
405
|
+
def remove
|
406
|
+
@_container&.remove
|
407
|
+
@_container = nil
|
408
|
+
self
|
409
|
+
rescue Excon::Error::Socket => e
|
410
|
+
raise ConnectionError, e.message
|
411
|
+
end
|
412
|
+
|
413
|
+
# @see #remove
|
414
|
+
alias_method :delete, :remove
|
415
|
+
|
416
|
+
# Restarts the container.
|
417
|
+
#
|
418
|
+
# @return [DockerContainer] The DockerContainer instance.
|
419
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
420
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
421
|
+
def restart
|
422
|
+
raise ContainerNotStartedError unless @_container
|
423
|
+
@_container.restart
|
424
|
+
self
|
425
|
+
rescue Excon::Error::Socket => e
|
426
|
+
raise ConnectionError, e.message
|
427
|
+
end
|
428
|
+
|
429
|
+
# Returns the container's status.
|
430
|
+
# Possible values are: "created", "restarting", "running", "removing", "paused", "exited", "dead".
|
431
|
+
#
|
432
|
+
# @return [String] The container's status.
|
433
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
434
|
+
def status
|
435
|
+
raise ContainerNotStartedError unless @_container
|
436
|
+
@_container.json["State"]["Status"]
|
437
|
+
rescue Excon::Error::Socket => e
|
438
|
+
raise ConnectionError, e.message
|
439
|
+
end
|
440
|
+
|
441
|
+
# Returns whether the container is running.
|
442
|
+
#
|
443
|
+
# @return [Boolean] Whether the container is running.
|
444
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
445
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
446
|
+
def dead?
|
447
|
+
status == "dead"
|
448
|
+
end
|
449
|
+
|
450
|
+
# Returns whether the container is paused.
|
451
|
+
#
|
452
|
+
# @return [Boolean] Whether the container is paused.
|
453
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
454
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
455
|
+
def paused?
|
456
|
+
status == "paused"
|
457
|
+
end
|
458
|
+
|
459
|
+
# Returns whether the container is restarting.
|
460
|
+
#
|
461
|
+
# @return [Boolean] Whether the container is restarting.
|
462
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
463
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
464
|
+
def restarting?
|
465
|
+
status == "restarting"
|
466
|
+
end
|
467
|
+
|
468
|
+
# Returns whether the container is running.
|
469
|
+
#
|
470
|
+
# @return [Boolean] Whether the container is running.
|
471
|
+
# @return [false] If the container has not been started instead of raising an ContainerNotStartedError.
|
472
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
473
|
+
def running?
|
474
|
+
status == "running"
|
475
|
+
rescue ContainerNotStartedError
|
476
|
+
false
|
477
|
+
end
|
478
|
+
|
479
|
+
# Returns whether the container is stopped.
|
480
|
+
#
|
481
|
+
# @return [Boolean] Whether the container is stopped.
|
482
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
483
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
484
|
+
def exited?
|
485
|
+
status == "exited"
|
486
|
+
end
|
487
|
+
|
488
|
+
# Returns whether the container is healthy.
|
489
|
+
#
|
490
|
+
# @return [Boolean] Whether the container is healthy.
|
491
|
+
# @return [false] If the container has not been started instead of raising an ContainerNotStartedError.
|
492
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
493
|
+
# @raise [HealthcheckNotSupportedError] If the container does not support healthchecks.
|
494
|
+
def healthy?
|
495
|
+
if supports_healtcheck?
|
496
|
+
@_container&.json&.dig("State", "Health", "Status") == "healthy"
|
497
|
+
else
|
498
|
+
raise HealthcheckNotSupportedError
|
499
|
+
end
|
500
|
+
rescue ContainerNotStartedError
|
501
|
+
false
|
502
|
+
rescue Excon::Error::Socket => e
|
503
|
+
raise ConnectionError, e.message
|
504
|
+
end
|
505
|
+
|
506
|
+
# Returns whether the container supports healthchecks.
|
507
|
+
# This is determined by the presence of a healthcheck in the container's configuration.
|
508
|
+
#
|
509
|
+
# @return [Boolean] Whether the container supports healthchecks.
|
510
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
511
|
+
def supports_healtcheck?
|
512
|
+
raise ContainerNotStartedError unless @_container
|
513
|
+
@_container.json["Config"]["Healthcheck"].present?
|
514
|
+
end
|
515
|
+
|
516
|
+
# Returns whether the container exists.
|
517
|
+
#
|
518
|
+
# @return [Boolean] Whether the container exists.
|
519
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
520
|
+
def exists?
|
521
|
+
return false unless @_id
|
522
|
+
|
523
|
+
Docker::Container.get(@_id)
|
524
|
+
true
|
525
|
+
rescue Docker::Error::NotFoundError
|
526
|
+
false
|
527
|
+
rescue Excon::Error::Socket => e
|
528
|
+
raise ConnectionError, e.message
|
529
|
+
end
|
530
|
+
|
531
|
+
# Returns the container's created at timestamp.
|
532
|
+
# The timestamp is in UTC and formatted as ISO 8601. Example: "2014-10-31T23:22:05.430Z".
|
533
|
+
#
|
534
|
+
# @return [String] The container's created at timestamp.
|
535
|
+
# @return [nil] If the container does not exist.
|
536
|
+
def created_at
|
537
|
+
@_created_at
|
538
|
+
end
|
539
|
+
|
540
|
+
# Returns the container's info (inspect).
|
541
|
+
# See https://docs.docker.com/engine/api/v1.42/#tag/Container/operation/ContainerInspect
|
542
|
+
#
|
543
|
+
# @return [Hash] The container's info.
|
544
|
+
# @return [nil] If the container does not exist.
|
545
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
546
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
547
|
+
def info
|
548
|
+
raise ContainerNotStartedError unless @_container
|
549
|
+
@_container.json
|
550
|
+
rescue Excon::Error::Socket => e
|
551
|
+
raise ConnectionError, e.message
|
552
|
+
end
|
553
|
+
|
554
|
+
# Returns the container's host.
|
555
|
+
#
|
556
|
+
# @return [String] The container's host.
|
557
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
558
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
559
|
+
def host
|
560
|
+
host = docker_host
|
561
|
+
return "localhost" if host.nil?
|
562
|
+
raise ContainerNotStartedError unless @_container
|
563
|
+
|
564
|
+
if inside_container? && ENV["DOCKER_HOST"].nil?
|
565
|
+
gateway_ip = container_gateway_ip
|
566
|
+
return bridge_ip if gateway_ip == host
|
567
|
+
return gateway_ip
|
568
|
+
end
|
569
|
+
host
|
570
|
+
rescue Excon::Error::Socket => e
|
571
|
+
raise ConnectionError, e.message
|
572
|
+
end
|
573
|
+
|
574
|
+
# Returns the mapped host port for the given container port.
|
575
|
+
#
|
576
|
+
# @param port [Integer] The container port.
|
577
|
+
# @return [String] The mapped host port.
|
578
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
579
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
580
|
+
def mapped_port(port)
|
581
|
+
raise ContainerNotStartedError unless @_container
|
582
|
+
mapped_port = container_port(port)
|
583
|
+
|
584
|
+
if inside_container?
|
585
|
+
gateway_ip = container_gateway_ip
|
586
|
+
host = docker_host
|
587
|
+
|
588
|
+
return port if gateway_ip == host
|
589
|
+
end
|
590
|
+
mapped_port
|
591
|
+
rescue Excon::Error::Socket => e
|
592
|
+
raise ConnectionError, e.message
|
593
|
+
end
|
594
|
+
|
595
|
+
# Returns the container's logs.
|
596
|
+
#
|
597
|
+
# @param stdout [Boolean] Whether to return stdout.
|
598
|
+
# @param stderr [Boolean] Whether to return stderr.
|
599
|
+
# @return [Array<String>] The container's logs.
|
600
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
601
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
602
|
+
def logs(stdout: true, stderr: true)
|
603
|
+
raise ContainerNotStartedError unless @_container
|
604
|
+
stdout = @_container.logs(stdout: stdout)
|
605
|
+
stderr = @_container.logs(stderr: stderr)
|
606
|
+
[stdout, stderr]
|
607
|
+
rescue Excon::Error::Socket => e
|
608
|
+
raise ConnectionError, e.message
|
609
|
+
end
|
610
|
+
|
611
|
+
# Executes a command in the container.
|
612
|
+
# See https://docs.docker.com/engine/api/v1.42/#operation/ContainerExec for all available options.
|
613
|
+
#
|
614
|
+
# @param cmd [Array<String>] The command to execute.
|
615
|
+
# @param options [Hash] Additional options to pass to the Docker Exec API. (e.g `Env`)
|
616
|
+
# @option options [Boolean] :tty Allocate a pseudo-TTY.
|
617
|
+
# @option options [Boolean] :detach (false) Whether to attach to STDOUT/STDERR.
|
618
|
+
# @option options [Object] :stdin Attach to stdin of the exec command.
|
619
|
+
# @option options [Integer] :wait The number of seconds to wait for the command to finish.
|
620
|
+
# @option options [String] :user The user to execute the command as.
|
621
|
+
# @return [Array, Array, Integer] The STDOUT, STDERR and exit code.
|
622
|
+
def exec(cmd, options = {}, &block)
|
623
|
+
raise ContainerNotStartedError unless @_container
|
624
|
+
@_container.exec(cmd, options, &block)
|
625
|
+
rescue Excon::Error::Socket => e
|
626
|
+
raise ConnectionError, e.message
|
627
|
+
end
|
628
|
+
|
629
|
+
# Waits for the container logs to match the given regex.
|
630
|
+
#
|
631
|
+
# @param matcher [Regexp] The regex to match.
|
632
|
+
# @param timeout [Integer] The number of seconds to wait for the logs to match.
|
633
|
+
# @param interval [Float] The number of seconds to wait between checks.
|
634
|
+
# @return [Boolean] Whether the logs matched the regex.
|
635
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
636
|
+
# @raise [TimeoutError] If the timeout is reached.
|
637
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
638
|
+
def wait_for_logs(matcher, timeout: 60, interval: 0.1)
|
639
|
+
raise ContainerNotStartedError unless @_container
|
640
|
+
|
641
|
+
Timeout.timeout(timeout) do
|
642
|
+
loop do
|
643
|
+
stdout, stderr = @_container.logs(stdout: true, stderr: true)
|
644
|
+
return true if stdout&.match?(matcher) || stderr&.match?(matcher)
|
645
|
+
|
646
|
+
sleep interval
|
647
|
+
end
|
648
|
+
end
|
649
|
+
rescue Timeout::Error
|
650
|
+
raise TimeoutError, "Timed out waiting for logs to match #{matcher}"
|
651
|
+
end
|
652
|
+
|
653
|
+
# Waits for the container to be healthy.
|
654
|
+
#
|
655
|
+
# @param timeout [Integer] The number of seconds to wait for the health check to be healthy.
|
656
|
+
# @param interval [Float] The number of seconds to wait between checks.
|
657
|
+
# @return [Boolean] Whether the container is healthy.
|
658
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
659
|
+
# @raise [TimeoutError] If the timeout is reached.
|
660
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
661
|
+
# @raise [HealthcheckNotSupportedError] If the container does not support healthchecks
|
662
|
+
def wait_for_healthcheck(timeout: 60, interval: 0.1)
|
663
|
+
raise ContainerNotStartedError unless @_container
|
664
|
+
raise HealthcheckNotSupportedError unless supports_healthcheck?
|
665
|
+
|
666
|
+
Timeout.timeout(timeout) do
|
667
|
+
loop do
|
668
|
+
return true if healthy?
|
669
|
+
|
670
|
+
sleep interval
|
671
|
+
end
|
672
|
+
end
|
673
|
+
rescue Timeout::Error
|
674
|
+
raise TimeoutError, "Timed out waiting for health check to be healthy"
|
675
|
+
end
|
676
|
+
|
677
|
+
# Waits for the container to open the given port.
|
678
|
+
#
|
679
|
+
# @param port [Integer] The port to wait for.
|
680
|
+
# @param timeout [Integer] The number of seconds to wait for the port to open.
|
681
|
+
# @param interval [Float] The number of seconds to wait between checks.
|
682
|
+
# @return [Boolean] Whether the port is open.
|
683
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
684
|
+
# @raise [TimeoutError] If the timeout is reached.
|
685
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
686
|
+
# @raise [PortNotMappedError] If the port is not mapped.
|
687
|
+
def wait_for_tcp_port(port, timeout: 60, interval: 0.1)
|
688
|
+
raise ContainerNotStartedError unless @_container
|
689
|
+
raise PortNotMappedError unless mapped_port(port)
|
690
|
+
|
691
|
+
Timeout.timeout(timeout) do
|
692
|
+
loop do
|
693
|
+
Timeout.timeout(interval) do
|
694
|
+
TCPSocket.new(host, mapped_port(port)).close
|
695
|
+
return true
|
696
|
+
end
|
697
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Timeout::Error
|
698
|
+
sleep interval
|
699
|
+
end
|
700
|
+
end
|
701
|
+
rescue Timeout::Error
|
702
|
+
raise TimeoutError, "Timed out waiting for port #{port} to open"
|
703
|
+
end
|
704
|
+
|
705
|
+
# Waits for the container to respond to HTTP requests.
|
706
|
+
#
|
707
|
+
# @param timeout [Integer] The number of seconds to wait for the TCP connection to be established.
|
708
|
+
# @param interval [Float] The number of seconds to wait between checks.
|
709
|
+
# @param path [String] The path to request.
|
710
|
+
# @param container_port [Integer] The container port to request.
|
711
|
+
# @param https [Boolean] Whether to use TLS.
|
712
|
+
# @param status [Integer] The expected HTTP status code.
|
713
|
+
# @return [Boolean] Whether the container is responding to HTTP requests.
|
714
|
+
# @raise [ContainerNotStartedError] If the container has not been started.
|
715
|
+
# @raise [TimeoutError] If the timeout is reached.
|
716
|
+
# @raise [ConnectionError] If the connection to the Docker daemon fails.
|
717
|
+
def wait_for_http(timeout: 60, interval: 0.1, path: "/", container_port: 80, https: false, status: 200)
|
718
|
+
raise ContainerNotStartedError unless @_container
|
719
|
+
raise PortNotMappedError unless mapped_port(container_port)
|
720
|
+
|
721
|
+
Timeout.timeout(timeout) do
|
722
|
+
loop do
|
723
|
+
begin
|
724
|
+
response = Excon.get("#{https ? "https" : "http"}://#{host}:#{mapped_port(container_port)}#{path}")
|
725
|
+
return true if response.status == status
|
726
|
+
rescue Excon::Error::Socket
|
727
|
+
# The container may not be ready to accept connections yet
|
728
|
+
end
|
729
|
+
|
730
|
+
sleep interval
|
731
|
+
end
|
732
|
+
end
|
733
|
+
rescue Timeout::Error
|
734
|
+
raise TimeoutError, "Timed out waiting for HTTP status #{status} on #{path}"
|
735
|
+
end
|
736
|
+
|
737
|
+
# Returns whether this is running inside a container.
|
738
|
+
#
|
739
|
+
# @return [Boolean] Whether this is running inside a container.
|
740
|
+
def inside_container?
|
741
|
+
File.exist?("/.dockerenv")
|
742
|
+
end
|
743
|
+
|
744
|
+
private
|
745
|
+
|
746
|
+
def normalize_ports(ports)
|
747
|
+
return if ports.nil?
|
748
|
+
return ports if ports.is_a?(Hash)
|
749
|
+
|
750
|
+
ports.each_with_object({}) do |port, hash|
|
751
|
+
hash[normalize_port(port)] = {}
|
752
|
+
end
|
753
|
+
end
|
754
|
+
|
755
|
+
def normalize_port(port)
|
756
|
+
port = port.to_s
|
757
|
+
port = "#{port}/tcp" unless port.include?("/")
|
758
|
+
port
|
759
|
+
end
|
760
|
+
|
761
|
+
def normalize_port_bindings(port_bindings)
|
762
|
+
return if port_bindings.nil?
|
763
|
+
return port_bindings if port_bindings.is_a?(Hash) && port_bindings.values.all? { |v| v.is_a?(Array) }
|
764
|
+
|
765
|
+
port_bindings.each_with_object({}) do |(container_port, host_port), hash|
|
766
|
+
hash[normalize_port(container_port)] = [{"HostPort" => host_port.to_s}]
|
767
|
+
end
|
768
|
+
end
|
769
|
+
|
770
|
+
def normalize_volumes(volumes)
|
771
|
+
return if volumes.nil?
|
772
|
+
return volumes if volumes.is_a?(Hash)
|
773
|
+
|
774
|
+
volumes.each_with_object({}) do |volume, hash|
|
775
|
+
hash[volume] = {}
|
776
|
+
end
|
777
|
+
end
|
778
|
+
|
779
|
+
def normalize_filesystem_binds(filesystem_binds)
|
780
|
+
return if filesystem_binds.nil?
|
781
|
+
|
782
|
+
if filesystem_binds.is_a?(Hash)
|
783
|
+
filesystem_binds.map { |host, container| [host, container] }
|
784
|
+
elsif filesystem_binds.is_a?(String)
|
785
|
+
[filesystem_binds.split(":")]
|
786
|
+
elsif filesystem_binds.is_a?(Array)
|
787
|
+
filesystem_binds.map { |bind| bind.split(":") }
|
788
|
+
else
|
789
|
+
raise ArgumentError, "Invalid filesystem_binds format"
|
790
|
+
end
|
791
|
+
end
|
792
|
+
|
793
|
+
def process_env_input(env_or_key, value = nil)
|
794
|
+
case env_or_key
|
795
|
+
when NilClass
|
796
|
+
nil
|
797
|
+
when Hash
|
798
|
+
raise ArgumentError, "value must be nil when env_or_key is a Hash" if value
|
799
|
+
env_or_key.map { |key, val| "#{key}=#{val}" }
|
800
|
+
when String
|
801
|
+
if value
|
802
|
+
raise ArgumentError, "value must be a String when env_or_key is a String" unless value.is_a?(String)
|
803
|
+
["#{env_or_key}=#{value}"]
|
804
|
+
else
|
805
|
+
raise ArgumentError, "Invalid input format: string should include '='" unless env_or_key.include?("=")
|
806
|
+
[env_or_key]
|
807
|
+
end
|
808
|
+
when Array
|
809
|
+
raise ArgumentError, "value must be nil when env_or_key is an Array" if value
|
810
|
+
env_or_key.each do |pair|
|
811
|
+
unless pair.is_a?(String) && pair.include?("=")
|
812
|
+
raise ArgumentError, "Invalid input format: array elements should be strings with '='"
|
813
|
+
end
|
814
|
+
end
|
815
|
+
env_or_key
|
816
|
+
else
|
817
|
+
raise ArgumentError, "Invalid input format for process_env_input"
|
818
|
+
end
|
819
|
+
end
|
820
|
+
|
821
|
+
def container_bridge_ip
|
822
|
+
@_container&.json&.dig("NetworkSettings", "Networks", "bridge", "IPAddress")
|
823
|
+
end
|
824
|
+
|
825
|
+
def container_gateway_ip
|
826
|
+
@_container&.json&.dig("NetworkSettings", "Networks", "bridge", "Gateway")
|
827
|
+
end
|
828
|
+
|
829
|
+
def container_port(port)
|
830
|
+
@_container&.json&.dig("NetworkSettings", "Ports", normalize_port(port))&.first&.dig("HostPort")
|
831
|
+
end
|
832
|
+
|
833
|
+
def default_gateway_ip
|
834
|
+
cmd = "ip route | awk '/default/ { print $3 }'"
|
835
|
+
|
836
|
+
ip_address, _stderr, status = Open3.capture3(cmd)
|
837
|
+
return ip_address.strip if ip_address && status.success?
|
838
|
+
rescue
|
839
|
+
nil
|
840
|
+
end
|
841
|
+
|
842
|
+
def docker_host
|
843
|
+
return ENV["TC_HOST"] if ENV["TC_HOST"]
|
844
|
+
url = URI.parse(Docker.url)
|
845
|
+
|
846
|
+
case url.scheme
|
847
|
+
when "http", "tcp"
|
848
|
+
url.host
|
849
|
+
when "unix", "npipe"
|
850
|
+
if inside_container?
|
851
|
+
ip_address = default_gateway_ip
|
852
|
+
return ip_address if ip_address
|
853
|
+
end
|
854
|
+
else
|
855
|
+
"localhost"
|
856
|
+
end
|
857
|
+
rescue URI::InvalidURIError
|
858
|
+
nil
|
859
|
+
end
|
860
|
+
|
861
|
+
def _container_create_options
|
862
|
+
{
|
863
|
+
"name" => @name,
|
864
|
+
"Image" => @image,
|
865
|
+
"Cmd" => @command,
|
866
|
+
"ExposedPorts" => @exposed_ports,
|
867
|
+
"Volumes" => @volumes,
|
868
|
+
"Env" => @env,
|
869
|
+
"Labels" => @labels,
|
870
|
+
"WorkingDir" => @working_dir,
|
871
|
+
"HostConfig" => {
|
872
|
+
"PortBindings" => @port_bindings,
|
873
|
+
"Binds" => @filesystem_binds
|
874
|
+
}.compact
|
875
|
+
}.compact
|
876
|
+
end
|
877
|
+
end
|
878
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "testcontainers"
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "docker"
|
4
|
+
require "logger"
|
5
|
+
require "open3"
|
6
|
+
require "uri"
|
7
|
+
require "testcontainers/docker_container"
|
8
|
+
require_relative "testcontainers/version"
|
9
|
+
|
10
|
+
module Testcontainers
|
11
|
+
class Error < StandardError; end
|
12
|
+
|
13
|
+
class ConnectionError < Error; end
|
14
|
+
|
15
|
+
class TimeoutError < Error; end
|
16
|
+
|
17
|
+
class ContainerNotStartedError < Error; end
|
18
|
+
|
19
|
+
class HealthcheckNotSupportedError < Error; end
|
20
|
+
|
21
|
+
class PortNotMappedError < Error; end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
attr_writer :logger
|
25
|
+
|
26
|
+
def logger
|
27
|
+
@logger ||= Logger.new($stdout, level: :info)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: testcontainers-core
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Guillermo Iguaran
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-05-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: docker-api
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '13.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '13.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-hooks
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.5'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.5'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: standard
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.3'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.3'
|
83
|
+
description: Testcontainers makes it easy to create and clean up container-based dependencies
|
84
|
+
for automated tests.
|
85
|
+
email:
|
86
|
+
- guilleiguaran@gmail.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- Gemfile
|
92
|
+
- Gemfile.lock
|
93
|
+
- LICENSE.txt
|
94
|
+
- README.md
|
95
|
+
- Rakefile
|
96
|
+
- lib/testcontainers-core.rb
|
97
|
+
- lib/testcontainers.rb
|
98
|
+
- lib/testcontainers/docker_container.rb
|
99
|
+
- lib/testcontainers/version.rb
|
100
|
+
homepage: https://github.com/guilleiguaran/testcontainers-ruby
|
101
|
+
licenses:
|
102
|
+
- MIT
|
103
|
+
metadata:
|
104
|
+
homepage_uri: https://github.com/guilleiguaran/testcontainers-ruby
|
105
|
+
source_code_uri: https://github.com/guilleiguaran/testcontainers-ruby
|
106
|
+
changelog_uri: https://github.com/guilleiguaran/testcontainers-ruby/blob/main/core/CHANGELOG.md
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: 2.6.0
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubygems_version: 3.4.1
|
123
|
+
signing_key:
|
124
|
+
specification_version: 4
|
125
|
+
summary: Testcontainers for Ruby.
|
126
|
+
test_files: []
|