From 85e284707248b989cf6d796d345b0b2734fa94c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarl=20Andr=C3=A9=20H=C3=BCbenthal?= Date: Tue, 3 Dec 2024 18:59:51 +0100 Subject: [PATCH] [chore] windows support (kind of) (#155) Support for windows and environments where localhost is not the correct hostname now uses docker host as found by docker context --- README.md | 27 +++++++++++++++++ lib/container.ex | 11 ++++++- lib/testcontainers.ex | 69 +++++++++++++++++++++++++++++-------------- mix.lock | 2 +- 4 files changed, 85 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 5370943..4d4453e 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,33 @@ Testcontainers use the standard Logger, see https://hexdocs.pm/logger/Logger.htm For more detailed information about the API, different container configurations, and advanced usage scenarios, please refer to the [API documentation](https://hexdocs.pm/testcontainers/api-reference.html). +## Windows support + +### Native +You can run on windows natively with elixir and erlang. + +First install Visual Studio 2022 with Desktop development with C++. + +Open visual studio dev shell. I do it by just opening an empty c++ project, then View -> Terminal. + +Enable "Expose daemon on tcp://localhost:2375 without TLS" in Docker settings. + +for powershell: + +`$Env:DOCKER_HOST = "tcp://localhost:2375"` + +for cmd: + +`set DOCKER_HOST=tcp://localhost:2375` + +Compile and run tests: + +`mix deps.get` + +`mix deps.compile` + +`mix test` + ## Contributing We welcome your contributions! Please see our contributing guidelines (TBD) for more details on how to submit patches and the contribution workflow. diff --git a/lib/container.ex b/lib/container.ex index 587beb7..b5fdbe2 100644 --- a/lib/container.ex +++ b/lib/container.ex @@ -46,13 +46,22 @@ defmodule Testcontainers.Container do defguard is_os(name) when is_atom(name) and name == @os_type + @dialyzer {:nowarn_function, os_type: 0} + def os_type() do + cond do + is_os(:linux) -> :linux + is_os(:macos) -> :macos + is_os(:windows) -> :windows + true -> :unknown + end + end + @doc """ A constructor function to make it easier to construct a container """ def new(image) when is_binary(image) do %__MODULE__{image: image} end - @doc """ Sets a _waiting strategy_ for the _container_. """ diff --git a/lib/testcontainers.ex b/lib/testcontainers.ex index 267c58b..33d8e0b 100644 --- a/lib/testcontainers.ex +++ b/lib/testcontainers.ex @@ -18,6 +18,7 @@ defmodule Testcontainers do alias Testcontainers.Util.PropertiesParser import Testcontainers.Constants + import Testcontainers.Container, only: [os_type: 0] @timeout 300_000 @@ -42,28 +43,16 @@ defmodule Testcontainers do ryuk_config = Container.new("testcontainers/ryuk:#{Constants.ryuk_version()}") |> Container.with_exposed_port(8080) - |> then(fn config -> - with %URI{scheme: "unix", path: docker_socket_path} <- URI.parse(docker_host) do - Container.with_bind_mount( - config, - docker_socket_path, - "/var/run/docker.sock", - "rw" - ) - else - _ -> - config - end - end) + |> then(&apply_docker_socket_volume_binding(&1, docker_host)) |> Container.with_auto_remove(true) with {:ok, _} <- Api.pull_image(ryuk_config.image, conn), + {:ok, docker_hostname} <- get_docker_hostname(docker_host_url, conn), {:ok, ryuk_container_id} <- Api.create_container(ryuk_config, conn), :ok <- Api.start_container(ryuk_container_id, conn), {:ok, container} <- Api.get_container(ryuk_container_id, conn), - {:ok, socket} <- create_ryuk_socket(container), + {:ok, socket} <- create_ryuk_socket(container, docker_hostname), :ok <- register_ryuk_filter(session_id, socket), - {:ok, docker_hostname} <- get_docker_hostname(docker_host_url, conn), {:ok, properties} <- PropertiesParser.read_property_file() do Logger.info("Testcontainers initialized") @@ -191,13 +180,13 @@ defmodule Testcontainers do GenServer.call(name, call, @timeout) end - defp create_ryuk_socket(container, reattempt_count \\ 0) + defp create_ryuk_socket(container, docker_hostname, reattempt_count \\ 0) - defp create_ryuk_socket(%Container{} = container, reattempt_count) + defp create_ryuk_socket(%Container{} = container, docker_hostname, reattempt_count) when reattempt_count < 3 do host_port = Container.mapped_port(container, 8080) - case :gen_tcp.connect(~c"localhost", host_port, [ + case :gen_tcp.connect(~c"#{docker_hostname}", host_port, [ :binary, active: false, packet: :line, @@ -207,17 +196,17 @@ defmodule Testcontainers do {:ok, connected} {:error, :econnrefused} -> - Logger.debug("Connection refused. Retrying... Attempt #{reattempt_count + 1}/3") + Logger.info("Connection refused. Retrying... Attempt #{reattempt_count + 1}/3") :timer.sleep(5000) - create_ryuk_socket(container, reattempt_count + 1) + create_ryuk_socket(container, docker_hostname, reattempt_count + 1) {:error, error} -> {:error, error} end end - defp create_ryuk_socket(%Container{} = _container, _reattempt_count) do - Logger.debug("Ryuk host refused to connect") + defp create_ryuk_socket(%Container{} = _container, _docker_hostname, _reattempt_count) do + Logger.info("Ryuk host refused to connect") {:error, :econnrefused} end @@ -291,4 +280,40 @@ defmodule Testcontainers do error end) end + + defp apply_docker_socket_volume_binding(config, docker_host) do + case {os_type(), URI.parse(docker_host)} do + {os, uri} -> handle_docker_socket_binding(config, os, uri) + end + end + + @dialyzer {:nowarn_function, handle_docker_socket_binding: 3} + defp handle_docker_socket_binding(config, :linux, %URI{scheme: "unix", path: docker_socket_path}) do + Container.with_bind_mount( + config, + docker_socket_path, + "/var/run/docker.sock", + "rw" + ) + end + + defp handle_docker_socket_binding(config, :macos, %URI{scheme: "unix", path: docker_socket_path}) do + Container.with_bind_mount( + config, + docker_socket_path, + "/var/run/docker.sock", + "rw" + ) + end + + defp handle_docker_socket_binding(config, :windows, _) do + Container.with_bind_mount( + config, + "//var/run/docker.sock", + "/var/run/docker.sock", + "rw" + ) + end + + defp handle_docker_socket_binding(config, _, _), do: config end diff --git a/mix.lock b/mix.lock index 5d4c446..4ad7059 100644 --- a/mix.lock +++ b/mix.lock @@ -4,7 +4,7 @@ "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cowlib": {:hex, :cowlib, "2.6.0", "8aa629f81a0fc189f261dc98a42243fa842625feea3c7ec56c48f4ccdb55490f", [:rebar3], [], "hexpm", "45a1a08e05e4c66f2af665295955e337d52c2d33b1f1cf24d353cadeddf34992"}, - "crc32cer": {:hex, :crc32cer, "0.1.10", "fb87abbf34b72f180f8c3a908cd1826c6cb9a59787d156a29e05de9e98be385e", [:rebar3], [], "hexpm", "5b1f47efd0a1b4b7411f1f35e14d3c8c6da6e6a2a725ec8f2cf1ab13703e5f38"}, + "crc32cer": {:hex, :crc32cer, "0.1.11", "b550da6d615feb72a882d15d020f8f7dee72dfb2cb1bcdf3b1ee8dc2afd68cfc", [:rebar3], [], "hexpm", "a39b8f0b1990ac1bf06c3a247fc6a178b740cdfc33c3b53688dc7dd6b1855942"}, "credentials_obfuscation": {:hex, :credentials_obfuscation, "3.4.0", "34e18b126b3aefd6e8143776fbe1ceceea6792307c99ac5ee8687911f048cfd7", [:rebar3], [], "hexpm", "738ace0ed5545d2710d3f7383906fc6f6b582d019036e5269c4dbd85dbced566"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"},