Skip to content

Commit

Permalink
Allow passing options to Docker::Image.create
Browse files Browse the repository at this point in the history
The [Docker ImageCreate](https://docs.docker.com/engine/api/v1.43/#tag/Image/operation/ImageCreate)
api accepts a variety of parameters.

For the `platform` behavior, there is default behavior which is notable:

```
Default: ""

When used in combination with the fromImage option, the daemon checks if the given image is present in the local image cache with the given OS and Architecture, and otherwise attempts to pull the image. If the option is not set, the host's native OS and Architecture are used.
...
```

Because it defauilts to the host's native architecture, it will attempt
to pull images matching that architecture and raises not found if an
image of that architecture doesn't exist. This can be a problem when,
for example, attempting to pull a `mysql` image on Mx Macs.
`Testcontainers::DockerContainer.create('mysql:5.7')` will fail because,
as of this writing, arm images are not created for `mysql:5.7`.

The solution to this is to pass `platform: 'linux/amd64'` to
`Docker::Image.create`. This tells Docker we specifically want a image
for that architecture (Rosetta on Mac OS X can run `linux/amd64` images).

This commit adds an optional `image_create_options` kwarg to
`DockerContainer#initialize` that defaults to an empty hash. This kwarg
allows for us to pass create parameters to `Docker::Image.create`.

I've also added a `NotFoundError` class because it seems that the
convention is to wrap dependency errors in a `Textcontainers` error class.

Updates the readme, including a readme addition to explain the changes
in #31.
  • Loading branch information
HeyNonster committed Dec 14, 2023
1 parent ee68f9f commit 9f09d43
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 2 deletions.
8 changes: 8 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## Unreleased

### Added

- DockerContainer#new now accepts optional keyword argument `image_create_options` which accepts a hash. Passes the options to `Docker::Image.create`. See the [Docker ImageCreate api](https://docs.docker.com/engine/api/v1.43/#tag/Image/operation/ImageCreate) for available parameters.

- DockerContainer#remove now accepts an optional options hash. See the [Docker ContainerDelete api](https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerDelete) for available parameters.

## [0.1.3] - 2023-06-10

### Added
Expand Down
2 changes: 2 additions & 0 deletions core/lib/testcontainers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class Error < StandardError; end

class ConnectionError < Error; end

class NotFoundError < Error; end

class TimeoutError < Error; end

class ContainerNotStartedError < Error; end
Expand Down
9 changes: 7 additions & 2 deletions core/lib/testcontainers/docker_container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,23 @@ class DockerContainer
# @param command [Array<String>, nil] the command to run in the container
# @param name [String, nil] the container's name
# @param exposed_ports [Hash, Array<String>, nil] a hash or an array of exposed container ports
# @param image_create_options [Hash] a hash of options to pass to Docker::Image.create.
# @param port_bindings [Hash, Array<String>, nil] a hash or an array of container ports to host port bindings
# @param volumes [Hash, Array<String>, nil] a hash or an array of volume paths in the container
# @param filesystem_binds [Array<String>, Hash, nil] an array of strings or a hash representing bind mounts from the host to the container
# @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 logger [Logger] a logger instance for the container
def initialize(image, name: nil, command: nil, entrypoint: nil, exposed_ports: nil, port_bindings: nil, volumes: nil, filesystem_binds: nil,
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)

@image = image
@name = name
@command = command
@entrypoint = entrypoint
@exposed_ports = add_exposed_ports(exposed_ports) if exposed_ports
@image_create_options = image_create_options
@port_bindings = add_fixed_exposed_ports(port_bindings) if port_bindings
@volumes = add_volumes(volumes) if volumes
@env = add_env(env) if env
Expand Down Expand Up @@ -468,8 +470,9 @@ def use
#
# @return [DockerContainer] The DockerContainer instance.
# @raise [ConnectionError] If the connection to the Docker daemon fails.
# @raise [NotFoundError] If Docker is unable to find the image.
def start
Docker::Image.create("fromImage" => @image)
Docker::Image.create({"fromImage" => @image}.merge(@image_create_options))

@_container ||= Docker::Container.create(_container_create_options)
@_container.start
Expand All @@ -482,6 +485,8 @@ def start
@wait_for&.call(self)

self
rescue Docker::Error::NotFoundError => e
raise NotFoundError, e.message
rescue Excon::Error::Socket => e
raise ConnectionError, e.message
end
Expand Down
14 changes: 14 additions & 0 deletions core/test/docker_container_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ def after_all
super
end

def test_it_creates_an_image_with_options
good_container = Testcontainers::DockerContainer.new("hello-world", image_create_options: {"tag" => "latest"})
bad_container = Testcontainers::DockerContainer.new("hello-world", image_create_options: {"tag" => "nonexistent_tag"})
good_container.start

assert good_container.exists?
assert_raises(Testcontainers::NotFoundError) { bad_container.start }
ensure
good_container.stop if good_container.exists? && good_container.running?
good_container.remove if good_container.exists?
bad_container.stop if bad_container.exists? && bad_container.running?
bad_container.remove if bad_container.exists?
end

def test_it_returns_the_container_image
assert_equal "hello-world", @container.image
end
Expand Down

0 comments on commit 9f09d43

Please sign in to comment.