diff --git a/README.md b/README.md index 711a539..967f5f4 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,17 @@ config = Testcontainers.RedisContainer.new() {:ok, container} = Testcontainers.start_container(config) ``` +If you want to use a predefined container, such as `RedisContainer`, with an alternative image, for example, `valkey/valkey`, it's possible: + +```elixir +{:ok, _} = Testcontainers.start_link() +config = + Testcontainers.RedisContainer.new() + |> Testcontainers.RedisContainer.with_image("valkey/valkey:latest") + |> Testcontainers.RedisContainer.with_check_image("valkey/valkey") +{:ok, container} = Testcontainers.start_container(config) +``` + ### ExUnit tests Given you have added Testcontainers.start_link() to test_helper.exs: diff --git a/lib/container.ex b/lib/container.ex index e0a593f..d5cc4b7 100644 --- a/lib/container.ex +++ b/lib/container.ex @@ -21,9 +21,17 @@ defmodule Testcontainers.Container do labels: %{}, auto_remove: false, container_id: nil, + check_image: ~r/.*/, network_mode: nil ] + @doc """ + Returns `true` if `term` is a valid `check_image`, otherwise returns `false`. + """ + @doc guard: true + defguard is_valid_image(check_image) + when is_binary(check_image) or is_struct(check_image, Regex) + @doc """ A constructor function to make it easier to construct a container """ @@ -152,6 +160,20 @@ defmodule Testcontainers.Container do %__MODULE__{config | auth: registry_auth_token} end + @doc """ + Set the regular expression to check the image validity. + + When using a string, it will compile it to a regular expression. If the compilation fails, it will raise a `Regex.CompileError`. + """ + def with_check_image(%__MODULE__{} = config, check_image) when is_binary(check_image) do + regex = Regex.compile!(check_image) + with_check_image(config, regex) + end + + def with_check_image(%__MODULE__{} = config, %Regex{} = check_image) do + %__MODULE__{config | check_image: check_image} + end + @doc """ Sets a network mode to apply to the container object in docker. """ @@ -173,10 +195,39 @@ defmodule Testcontainers.Container do |> List.last() end + @doc """ + Check if the provided image is compatible with the expected default image. + + Raises: + + ArgumentError when image isn't compatible. + """ + def valid_image!(%__MODULE__{} = config) do + case valid_image(config) do + {:ok, config} -> + config + + {:error, message} -> + raise ArgumentError, message: message + end + end + + @doc """ + Check if the provided image is compatible with the expected default image. + """ + def valid_image(%__MODULE__{image: image, check_image: check_image} = config) do + if Regex.match?(check_image, image) do + {:ok, config} + else + {:error, + "Unexpected image #{image}. If this is a valid image, provide a broader `check_image` regex to the container configuration."} + end + end + defimpl Testcontainers.ContainerBuilder do @impl true def build(%Testcontainers.Container{} = config) do - config + Testcontainers.Container.valid_image!(config) end @doc """ diff --git a/lib/container/cassandra_container.ex b/lib/container/cassandra_container.ex index def19d9..069c06f 100644 --- a/lib/container/cassandra_container.ex +++ b/lib/container/cassandra_container.ex @@ -9,6 +9,8 @@ defmodule Testcontainers.CassandraContainer do alias Testcontainers.ContainerBuilder alias Testcontainers.Container + import Testcontainers.Container, only: [is_valid_image: 1] + @default_image "cassandra" @default_tag "3.11.2" @default_image_with_tag "#{@default_image}:#{@default_tag}" @@ -18,7 +20,7 @@ defmodule Testcontainers.CassandraContainer do @default_wait_timeout 60_000 @enforce_keys [:image, :wait_timeout] - defstruct [:image, :wait_timeout] + defstruct [:image, :wait_timeout, check_image: @default_image] def new, do: %__MODULE__{ @@ -30,6 +32,13 @@ defmodule Testcontainers.CassandraContainer do %{config | image: image} end + @doc """ + Set the regular expression to check the image validity. + """ + def with_check_image(%__MODULE__{} = config, check_image) when is_valid_image(check_image) do + %__MODULE__{config | check_image: check_image} + end + def default_image, do: @default_image def default_port, do: @default_port @@ -56,12 +65,6 @@ defmodule Testcontainers.CassandraContainer do @impl true @spec build(%CassandraContainer{}) :: %Container{} def build(%CassandraContainer{} = config) do - if not String.starts_with?(config.image, CassandraContainer.default_image()) do - raise ArgumentError, - message: - "Image #{config.image} is not compatible with #{CassandraContainer.default_image()}" - end - new(config.image) |> with_exposed_port(CassandraContainer.default_port()) |> with_environment(:CASSANDRA_SNITCH, "GossipingPropertyFileSnitch") @@ -79,6 +82,8 @@ defmodule Testcontainers.CassandraContainer do config.wait_timeout ) ) + |> with_check_image(config.check_image) + |> valid_image!() end @impl true diff --git a/lib/container/ceph_container.ex b/lib/container/ceph_container.ex index 34341ae..7124dce 100644 --- a/lib/container/ceph_container.ex +++ b/lib/container/ceph_container.ex @@ -9,6 +9,8 @@ defmodule Testcontainers.CephContainer do alias Testcontainers.ContainerBuilder alias Testcontainers.Container + import Testcontainers.Container, only: [is_valid_image: 1] + @default_image "quay.io/ceph/demo" @default_tag "latest-quincy" @default_image_with_tag "#{@default_image}:#{@default_tag}" @@ -19,7 +21,15 @@ defmodule Testcontainers.CephContainer do @default_wait_timeout 300_000 @enforce_keys [:image, :access_key, :secret_key, :bucket, :port, :wait_timeout] - defstruct [:image, :access_key, :secret_key, :bucket, :port, :wait_timeout] + defstruct [ + :image, + :access_key, + :secret_key, + :bucket, + :port, + :wait_timeout, + check_image: @default_image + ] @doc """ Creates a new `CephContainer` struct with default attributes. @@ -128,6 +138,13 @@ defmodule Testcontainers.CephContainer do %{config | wait_timeout: wait_timeout} end + @doc """ + Set the regular expression to check the image validity. + """ + def with_check_image(%__MODULE__{} = config, check_image) when is_valid_image(check_image) do + %__MODULE__{config | check_image: check_image} + end + @doc """ Retrieves the default Docker image used for the Ceph container. @@ -208,11 +225,6 @@ defmodule Testcontainers.CephContainer do @spec build(%CephContainer{}) :: %Container{} @impl true def build(%CephContainer{} = config) do - if not String.starts_with?(config.image, CephContainer.default_image()) do - raise ArgumentError, - message: "Image #{config.image} is not compatible with #{CephContainer.default_image()}" - end - new(config.image) |> with_exposed_port(config.port) |> with_environment(:CEPH_DEMO_UID, "demo") @@ -229,6 +241,8 @@ defmodule Testcontainers.CephContainer do 5000 ) ) + |> with_check_image(config.check_image) + |> valid_image!() end @impl true diff --git a/lib/container/emqx_container.ex b/lib/container/emqx_container.ex index 7a3e0ff..f33f909 100644 --- a/lib/container/emqx_container.ex +++ b/lib/container/emqx_container.ex @@ -8,6 +8,8 @@ defmodule Testcontainers.EmqxContainer do alias Testcontainers.PortWaitStrategy alias Testcontainers.EmqxContainer + import Testcontainers.Container, only: [is_valid_image: 1] + @default_image "emqx" @default_tag "5.6.0" @default_image_with_tag "#{@default_image}:#{@default_tag}" @@ -26,7 +28,8 @@ defmodule Testcontainers.EmqxContainer do :mqtt_over_ws_port, :mqtt_over_wss_port, :dashboard_port, - :wait_timeout + :wait_timeout, + check_image: @default_image ] @doc """ @@ -76,6 +79,13 @@ defmodule Testcontainers.EmqxContainer do } end + @doc """ + Set the regular expression to check the image validity. + """ + def with_check_image(%__MODULE__{} = config, check_image) when is_valid_image(check_image) do + %__MODULE__{config | check_image: check_image} + end + @doc """ Retrieves the default Docker image for the Emqx container. """ @@ -100,14 +110,11 @@ defmodule Testcontainers.EmqxContainer do """ @impl true def build(%EmqxContainer{} = config) do - if not String.starts_with?(config.image, EmqxContainer.default_image()) do - raise ArgumentError, - message: "Image #{config.image} is not compatible with #{EmqxContainer.default_image()}" - end - new(config.image) |> with_exposed_ports(exposed_ports(config)) |> with_waiting_strategies(waiting_strategies(config)) + |> with_check_image(config.check_image) + |> valid_image!() end defp exposed_ports(config), diff --git a/lib/container/mysql_container.ex b/lib/container/mysql_container.ex index bce77a8..6188f9b 100644 --- a/lib/container/mysql_container.ex +++ b/lib/container/mysql_container.ex @@ -12,6 +12,8 @@ defmodule Testcontainers.MySqlContainer do alias Testcontainers.MySqlContainer alias Testcontainers.LogWaitStrategy + import Testcontainers.Container, only: [is_valid_image: 1] + @default_image "mysql" @default_tag "8" @default_image_with_tag "#{@default_image}:#{@default_tag}" @@ -22,7 +24,16 @@ defmodule Testcontainers.MySqlContainer do @default_wait_timeout 180_000 @enforce_keys [:image, :user, :password, :database, :port, :wait_timeout, :persistent_volume] - defstruct [:image, :user, :password, :database, :port, :wait_timeout, :persistent_volume] + defstruct [ + :image, + :user, + :password, + :database, + :port, + :wait_timeout, + :persistent_volume, + check_image: @default_image + ] @doc """ Creates a new `MySqlContainer` struct with default configurations. @@ -131,6 +142,13 @@ defmodule Testcontainers.MySqlContainer do %{config | wait_timeout: wait_timeout} end + @doc """ + Set the regular expression to check the image validity. + """ + def with_check_image(%__MODULE__{} = config, check_image) when is_valid_image(check_image) do + %__MODULE__{config | check_image: check_image} + end + @doc """ Retrieves the default exposed port for the MySQL container. """ @@ -188,12 +206,6 @@ defmodule Testcontainers.MySqlContainer do @spec build(%MySqlContainer{}) :: %Container{} @impl true def build(%MySqlContainer{} = config) do - if not String.starts_with?(config.image, MySqlContainer.default_image()) do - raise ArgumentError, - message: - "Image #{config.image} is not compatible with #{MySqlContainer.default_image()}" - end - new(config.image) |> then(MySqlContainer.container_port_fun(config.port)) |> with_environment(:MYSQL_USER, config.user) @@ -204,6 +216,8 @@ defmodule Testcontainers.MySqlContainer do |> with_waiting_strategy( LogWaitStrategy.new(~r/.*port: 3306 MySQL Community Server.*/, config.wait_timeout) ) + |> with_check_image(config.check_image) + |> valid_image!() end @impl true diff --git a/lib/container/postgres_container.ex b/lib/container/postgres_container.ex index 3cdbfc2..94e64d2 100644 --- a/lib/container/postgres_container.ex +++ b/lib/container/postgres_container.ex @@ -12,6 +12,8 @@ defmodule Testcontainers.PostgresContainer do alias Testcontainers.Container alias Testcontainers.ContainerBuilder + import Testcontainers.Container, only: [is_valid_image: 1] + @default_image "postgres" @default_tag "15-alpine" @default_image_with_tag "#{@default_image}:#{@default_tag}" @@ -22,7 +24,16 @@ defmodule Testcontainers.PostgresContainer do @default_wait_timeout 60_000 @enforce_keys [:image, :user, :password, :database, :port, :wait_timeout, :persistent_volume] - defstruct [:image, :user, :password, :database, :port, :wait_timeout, :persistent_volume] + defstruct [ + :image, + :user, + :password, + :database, + :port, + :wait_timeout, + :persistent_volume, + check_image: @default_image + ] @doc """ Creates a new `PostgresContainer` struct with default configurations. @@ -131,6 +142,13 @@ defmodule Testcontainers.PostgresContainer do %{config | wait_timeout: wait_timeout} end + @doc """ + Set the regular expression to check the image validity. + """ + def with_check_image(%__MODULE__{} = config, check_image) when is_valid_image(check_image) do + %__MODULE__{config | check_image: check_image} + end + @doc """ Retrieves the default exposed port for the Postgres container. """ @@ -188,12 +206,6 @@ defmodule Testcontainers.PostgresContainer do @spec build(%PostgresContainer{}) :: %Container{} @impl true def build(%PostgresContainer{} = config) do - if not String.starts_with?(config.image, PostgresContainer.default_image()) do - raise ArgumentError, - message: - "Image #{config.image} is not compatible with #{PostgresContainer.default_image()}" - end - new(config.image) |> then(PostgresContainer.container_port_fun(config.port)) |> with_environment(:POSTGRES_USER, config.user) @@ -210,6 +222,8 @@ defmodule Testcontainers.PostgresContainer do config.wait_timeout ) ) + |> with_check_image(config.check_image) + |> valid_image!() end @impl true diff --git a/lib/container/rabbitmq_container.ex b/lib/container/rabbitmq_container.ex index 8e48e70..3c080c5 100644 --- a/lib/container/rabbitmq_container.ex +++ b/lib/container/rabbitmq_container.ex @@ -10,6 +10,8 @@ defmodule Testcontainers.RabbitMQContainer do alias Testcontainers.CommandWaitStrategy alias Testcontainers.RabbitMQContainer + import Testcontainers.Container, only: [is_valid_image: 1] + @default_image "rabbitmq" @default_tag "3-alpine" @default_image_with_tag "#{@default_image}:#{@default_tag}" @@ -25,7 +27,16 @@ defmodule Testcontainers.RabbitMQContainer do @default_wait_timeout 60_000 @enforce_keys [:image, :port, :wait_timeout] - defstruct [:image, :port, :username, :password, :virtual_host, :cmd, :wait_timeout] + defstruct [ + :image, + :port, + :username, + :password, + :virtual_host, + :cmd, + :wait_timeout, + check_image: @default_image + ] @doc """ Creates a new `RabbitMQContainer` struct with default configurations. @@ -134,6 +145,13 @@ defmodule Testcontainers.RabbitMQContainer do %{config | cmd: cmd} end + @doc """ + Set the regular expression to check the image validity. + """ + def with_check_image(%__MODULE__{} = config, check_image) when is_valid_image(check_image) do + %__MODULE__{config | check_image: check_image} + end + @doc """ Retrieves the default Docker image for the RabbitMQ container """ @@ -242,12 +260,6 @@ defmodule Testcontainers.RabbitMQContainer do @impl true @spec build(%RabbitMQContainer{}) :: %Container{} def build(%RabbitMQContainer{} = config) do - if not String.starts_with?(config.image, RabbitMQContainer.default_image()) do - raise ArgumentError, - message: - "Image #{config.image} is not compatible with #{RabbitMQContainer.default_image()}" - end - new(config.image) |> with_exposed_port(config.port) |> with_environment(:RABBITMQ_DEFAULT_USER, config.username) @@ -261,6 +273,8 @@ defmodule Testcontainers.RabbitMQContainer do config.wait_timeout ) ) + |> with_check_image(config.check_image) + |> valid_image!() end @impl true diff --git a/lib/container/redis_container.ex b/lib/container/redis_container.ex index 993bc44..9171814 100644 --- a/lib/container/redis_container.ex +++ b/lib/container/redis_container.ex @@ -11,6 +11,8 @@ defmodule Testcontainers.RedisContainer do alias Testcontainers.CommandWaitStrategy alias Testcontainers.RedisContainer + import Testcontainers.Container, only: [is_valid_image: 1] + @default_image "redis" @default_tag "7.2-alpine" @default_image_with_tag "#{@default_image}:#{@default_tag}" @@ -18,7 +20,7 @@ defmodule Testcontainers.RedisContainer do @default_wait_timeout 60_000 @enforce_keys [:image, :port, :wait_timeout] - defstruct [:image, :port, :wait_timeout] + defstruct [:image, :port, :wait_timeout, check_image: @default_image] @doc """ Creates a new `RedisContainer` struct with default configurations. @@ -76,6 +78,13 @@ defmodule Testcontainers.RedisContainer do %{config | wait_timeout: wait_timeout} end + @doc """ + Set the regular expression to check the image validity. + """ + def with_check_image(%__MODULE__{} = config, check_image) when is_valid_image(check_image) do + %__MODULE__{config | check_image: check_image} + end + @doc """ Retrieves the default Docker image for the Redis container. """ @@ -127,17 +136,13 @@ defmodule Testcontainers.RedisContainer do @spec build(%RedisContainer{}) :: %Container{} @impl true def build(%RedisContainer{} = config) do - if not String.starts_with?(config.image, RedisContainer.default_image()) do - raise ArgumentError, - message: - "Image #{config.image} is not compatible with #{RedisContainer.default_image()}" - end - new(config.image) |> with_exposed_port(config.port) |> with_waiting_strategy( CommandWaitStrategy.new(["redis-cli", "PING"], config.wait_timeout) ) + |> with_check_image(config.check_image) + |> valid_image!() end @impl true diff --git a/lib/container/selenium_container.ex b/lib/container/selenium_container.ex index 02860e0..b786d57 100644 --- a/lib/container/selenium_container.ex +++ b/lib/container/selenium_container.ex @@ -10,6 +10,8 @@ defmodule Testcontainers.SeleniumContainer do alias Testcontainers.PortWaitStrategy alias Testcontainers.LogWaitStrategy + import Testcontainers.Container, only: [is_valid_image: 1] + @default_image "selenium/standalone-chrome" @default_tag "118.0" @default_image_with_tag "#{@default_image}:#{@default_tag}" @@ -18,7 +20,7 @@ defmodule Testcontainers.SeleniumContainer do @default_wait_timeout 120_000 @enforce_keys [:image, :port1, :port2, :wait_timeout] - defstruct [:image, :port1, :port2, :wait_timeout] + defstruct [:image, :port1, :port2, :wait_timeout, check_image: @default_image] def new, do: %__MODULE__{ @@ -44,6 +46,13 @@ defmodule Testcontainers.SeleniumContainer do %{config | wait_timeout: wait_timeout} end + @doc """ + Set the regular expression to check the image validity. + """ + def with_check_image(%__MODULE__{} = config, check_image) when is_valid_image(check_image) do + %__MODULE__{config | check_image: check_image} + end + def default_image, do: @default_image defimpl ContainerBuilder do @@ -54,12 +63,6 @@ defmodule Testcontainers.SeleniumContainer do @spec build(%SeleniumContainer{}) :: %Container{} @impl true def build(%SeleniumContainer{} = config) do - if not String.starts_with?(config.image, SeleniumContainer.default_image()) do - raise ArgumentError, - message: - "Image #{config.image} is not compatible with #{SeleniumContainer.default_image()}" - end - new(config.image) |> with_exposed_ports([config.port1, config.port2]) |> with_waiting_strategies([ @@ -67,6 +70,8 @@ defmodule Testcontainers.SeleniumContainer do PortWaitStrategy.new("127.0.0.1", config.port1, config.wait_timeout, 1000), PortWaitStrategy.new("127.0.0.1", config.port2, config.wait_timeout, 1000) ]) + |> with_check_image(config.check_image) + |> valid_image!() end @impl true diff --git a/lib/ecto.ex b/lib/ecto.ex index daa8adc..266079c 100644 --- a/lib/ecto.ex +++ b/lib/ecto.ex @@ -23,6 +23,7 @@ defmodule Testcontainers.Ecto do - `:database` (optional) - Specifies the name of the database to be created within the Postgres instance. If not provided, the default behavior is to create a database with the name derived from the application's atom, appended with "_test". - `:migrations_path` (optional) - Indicates the path to the migrations folder (defaults to "priv/repo/migrations"). - `:persistent_volume_name` (optional, EXPERIMENTAL) - Sets a named volume for the data in the database. This is an experimental option, and changes in database container image or other things that could invalidate the data, would make the container not start properly. + - `:check_image` (optional) - Defines a custom regular expression that is used to validate the Docker image. ## Database Lifecycle in Testing @@ -119,6 +120,7 @@ defmodule Testcontainers.Ecto do - `:database` (optional) - Specifies the name of the database to be created within the Mysql instance. If not provided, the default behavior is to create a database with the name derived from the application's atom, appended with "_test". - `:migrations_path` (optional) - Indicates the path to the migrations folder (defaults to "priv/repo/migrations"). - `:persistent_volume_name` (optional, EXPERIMENTAL) - Sets a named volume for the data in the database. This is an experimental option, and changes in database container image or other things that could invalidate the data, would make the container not start properly. + - `:check_image` (optional) - Defines a custom regular expression that is used to validate the Docker image. ## Database Lifecycle in Testing @@ -202,7 +204,7 @@ defmodule Testcontainers.Ecto do ## Note - This utility is intended for testing environments requiring a genuine database instance. It is not suitable for production use. It mandates a valid Postgres Docker image to maintain consistent and reliable testing conditions. + This utility is intended for testing environments requiring a genuine database instance. It is not suitable for production use. It mandates a valid Mysql Docker image to maintain consistent and reliable testing conditions. """ def mysql_container(options \\ []) do database_container(:mysql, options) @@ -236,6 +238,7 @@ defmodule Testcontainers.Ecto do database = Keyword.get(options, :database, "#{Atom.to_string(app)}_test") migrations_path = Keyword.get(options, :migrations_path, "priv/repo/migrations") persistent_volume_name = Keyword.get(options, :persistent_volume_name, nil) + check_image = Keyword.get(options, :check_image, nil) container_module = case type do @@ -245,12 +248,6 @@ defmodule Testcontainers.Ecto do image = Keyword.get(options, :image, container_module.default_image_with_tag()) - maybe_persistent_volume_name_fn = - case persistent_volume_name do - nil -> fn config -> config end - name -> fn config -> config |> container_module.with_persistent_volume(name) end - end - config = container_module.new() |> container_module.with_image(image) @@ -258,7 +255,8 @@ defmodule Testcontainers.Ecto do |> container_module.with_user(user) |> container_module.with_database(database) |> container_module.with_password(password) - |> Kernel.then(maybe_persistent_volume_name_fn) + |> maybe_with_call(persistent_volume_name, &container_module.with_persistent_volume/2) + |> maybe_with_call(check_image, &container_module.with_check_image/2) case Testcontainers.start_container(config) do {:ok, container} -> @@ -316,4 +314,12 @@ defmodule Testcontainers.Ecto do |> Enum.map(&String.capitalize/1) |> Enum.join() end + + defp maybe_with_call(config, nil, _function) do + config + end + + defp maybe_with_call(config, value, function) do + function.(config, value) + end end diff --git a/test/container_test.exs b/test/container_test.exs index 35bb942..802b71a 100644 --- a/test/container_test.exs +++ b/test/container_test.exs @@ -96,4 +96,88 @@ defmodule Testcontainers.ContainerTest do "eyJwYXNzd29yZCI6InBhc3N3b3JkIiwidXNlcm5hbWUiOiJ1c2VybmFtZSJ9" end end + + describe "with_check_image/2" do + test "compiles a string into a valid regex" do + container = + "registry.io/my-user/my-image:latest" + |> Container.new() + |> Container.with_check_image("my-image") + + assert container.check_image == ~r/my-image/ + end + + test "raises Regex.CompileError when string can't be compiled to a valid regex" do + assert_raise Regex.CompileError, fn -> + "registry.io/my-user/my-image:latest" + |> Container.new() + |> Container.with_check_image("*my-image") + end + end + + test "accepts a regex" do + container = + "registry.io/my-user/my-image:latest" + |> Container.new() + |> Container.with_check_image(~r/.*my-image.*/) + + assert container.check_image == ~r/.*my-image.*/ + end + end + + describe "valid_image/1" do + test "return config when check image isn't set" do + container = Container.new("invalid-image") + + assert {:ok, container} == Container.valid_image(container) + end + + test "return config when image matches default string" do + container = + Container.new("valid-image") + |> Container.with_check_image("valid") + + assert {:ok, container} == Container.valid_image(container) + end + + test "return config when image contains the prefix" do + container = + Container.new("custom-hub.io/for-user/valid-image:tagged") + |> Container.with_check_image("valid") + + assert {:ok, container} == Container.valid_image(container) + end + + test "return config when image matches a custom regular expression" do + container = + Container.new("valid-image") + |> Container.with_check_image(~r/.*valid-image.*/) + + assert {:ok, container} == Container.valid_image(container) + end + + test "return error when image doesn't match default one" do + container = + Container.new("invalid-image") + |> Container.with_check_image("validated") + + assert {:error, + "Unexpected image invalid-image. If this is a valid image, provide a broader `check_image` regex to the container configuration."} == + Container.valid_image(container) + end + end + + describe "valid_image!/1" do + test "raises error when image isn't valid" do + container = + Container.new("invalid-image") + |> Container.with_check_image("validated") + + assert_raise ArgumentError, + "Unexpected image invalid-image. If this is a valid image, provide a broader `check_image` regex to the container configuration.", + fn -> + Container.valid_image!(container) + end + end + end end