Packages

A Gleam library for running test containers via Docker.

Current section

Files

Jump to
melon src melon@container.erl
Raw

src/melon@container.erl

-module(melon@container).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-export([new/1, set_user/2, set_entrypoint/2, set_command/2, set_working_directory/2, set_memory_limit/3, set_env_file/2, set_health_check_command/2, set_health_check_interval/3, set_health_check_timeout/3, set_health_check_start_period/3, set_health_check_retries/2, add_exposed_port/4, add_env/3, stop/1, mapped_port/3, start/1, wait_until_healthy/2]).
-export_type([container_start_failure/0, container_stop_failure/0, container_health_status_failure/0, container_mapped_port_failure/0, port_protocol/0, port_/0, memory_unit/0, time_unit/0, container/0, environment_variable/0, memory_limit/0, health_check_interval/0, health_check_timeout/0, health_check_start_period/0]).
-type container_start_failure() :: {could_not_start_container, binary()}.
-type container_stop_failure() :: cannot_stop_container_that_is_not_running |
{could_not_stop_container, binary()}.
-type container_health_status_failure() :: cannot_obtain_health_status_of_container_that_is_not_running |
{could_not_obtain_health_status, binary()} |
container_is_unhealthy.
-type container_mapped_port_failure() :: cannot_obtain_mapped_port_of_container_that_is_not_running |
could_not_find_mapped_port.
-type port_protocol() :: tcp | udp | sctp.
-type port_() :: {port, binary(), integer(), port_protocol()}.
-type memory_unit() :: byte | kilobyte | megabyte | gigabyte.
-type time_unit() :: second | minute.
-opaque container() :: {container,
gleam@option:option(binary()),
binary(),
gleam@option:option(binary()),
gleam@option:option(binary()),
gleam@option:option(list(binary())),
gleam@option:option(binary()),
gleam@option:option(memory_limit()),
gleam@option:option(binary()),
gleam@option:option(list(binary())),
gleam@option:option(health_check_interval()),
gleam@option:option(health_check_timeout()),
gleam@option:option(health_check_start_period()),
gleam@option:option(integer()),
list(port_()),
list(environment_variable())}.
-type environment_variable() :: {environment_variable, binary(), binary()}.
-type memory_limit() :: {memory_limit, integer(), memory_unit()}.
-type health_check_interval() :: {health_check_interval, integer(), time_unit()}.
-type health_check_timeout() :: {health_check_timeout, integer(), time_unit()}.
-type health_check_start_period() :: {health_check_start_period,
integer(),
time_unit()}.
-spec new(binary()) -> container().
new(Image) ->
{container,
none,
Image,
none,
none,
none,
none,
none,
none,
none,
none,
none,
none,
none,
[],
[]}.
-spec set_user(container(), binary()) -> container().
set_user(Container, User) ->
erlang:setelement(4, Container, {some, User}).
-spec set_entrypoint(container(), binary()) -> container().
set_entrypoint(Container, Entrypoint) ->
erlang:setelement(5, Container, {some, Entrypoint}).
-spec set_command(container(), list(binary())) -> container().
set_command(Container, Command) ->
erlang:setelement(6, Container, {some, Command}).
-spec set_working_directory(container(), binary()) -> container().
set_working_directory(Container, Working_directory) ->
erlang:setelement(7, Container, {some, Working_directory}).
-spec set_memory_limit(container(), integer(), memory_unit()) -> container().
set_memory_limit(Container, Limit, Unit) ->
erlang:setelement(8, Container, {some, {memory_limit, Limit, Unit}}).
-spec set_env_file(container(), binary()) -> container().
set_env_file(Container, Env_file) ->
erlang:setelement(9, Container, {some, Env_file}).
-spec set_health_check_command(container(), list(binary())) -> container().
set_health_check_command(Container, Command) ->
erlang:setelement(10, Container, {some, Command}).
-spec set_health_check_interval(container(), integer(), time_unit()) -> container().
set_health_check_interval(Container, Interval, Unit) ->
erlang:setelement(
11,
Container,
{some, {health_check_interval, Interval, Unit}}
).
-spec set_health_check_timeout(container(), integer(), time_unit()) -> container().
set_health_check_timeout(Container, Timeout, Unit) ->
erlang:setelement(
12,
Container,
{some, {health_check_timeout, Timeout, Unit}}
).
-spec set_health_check_start_period(container(), integer(), time_unit()) -> container().
set_health_check_start_period(Container, Start_period, Unit) ->
erlang:setelement(
13,
Container,
{some, {health_check_start_period, Start_period, Unit}}
).
-spec set_health_check_retries(container(), integer()) -> container().
set_health_check_retries(Container, Retries) ->
erlang:setelement(14, Container, {some, Retries}).
-spec add_exposed_port(container(), binary(), integer(), port_protocol()) -> container().
add_exposed_port(Container, Host, Port, Protocol) ->
erlang:setelement(
15,
Container,
[{port, Host, Port, Protocol} | erlang:element(15, Container)]
).
-spec add_env(container(), binary(), binary()) -> container().
add_env(Container, Identifier, Value) ->
erlang:setelement(
16,
Container,
[{environment_variable, Identifier, Value} |
erlang:element(16, Container)]
).
-spec docker_cmd(list(binary())) -> {ok, gleamyshell:command_output()} |
{error, binary()}.
docker_cmd(Args) ->
gleamyshell_ffi:execute(<<"docker"/utf8>>, <<"."/utf8>>, Args).
-spec stop(container()) -> {ok, container()} | {error, container_stop_failure()}.
stop(Container) ->
case erlang:element(2, Container) of
none ->
{error, cannot_stop_container_that_is_not_running};
{some, Container_id} ->
case docker_cmd([<<"stop"/utf8>>, Container_id]) of
{ok, {command_output, 0, _}} ->
_pipe = erlang:setelement(2, Container, none),
{ok, _pipe};
{ok, {command_output, _, Output}} ->
_pipe@1 = gleam@string:trim(Output),
_pipe@2 = {could_not_stop_container, _pipe@1},
{error, _pipe@2};
{error, Reason} ->
_pipe@3 = gleam@string:trim(Reason),
_pipe@4 = {could_not_stop_container, _pipe@3},
{error, _pipe@4}
end
end.
-spec mapped_port(container(), integer(), port_protocol()) -> {ok, port_()} |
{error, container_mapped_port_failure()}.
mapped_port(Container, Port, Protocol) ->
case erlang:element(2, Container) of
none ->
{error, cannot_obtain_mapped_port_of_container_that_is_not_running};
{some, Container_id} ->
case docker_cmd(
[<<"port"/utf8>>,
Container_id,
<<<<(gleam@int:to_string(Port))/binary, "/"/utf8>>/binary,
(begin
_pipe = gleam@string:inspect(Protocol),
gleam@string:lowercase(_pipe)
end)/binary>>]
) of
{ok, {command_output, 0, Output}} ->
case begin
_pipe@1 = gleam@string:trim(Output),
gleam@string:split(_pipe@1, <<":"/utf8>>)
end of
[Host, Value | _] ->
case gleam@int:parse(Value) of
{error, _} ->
{error, could_not_find_mapped_port};
{ok, Int_value} ->
_pipe@2 = {port, Host, Int_value, Protocol},
{ok, _pipe@2}
end;
_ ->
{error, could_not_find_mapped_port}
end;
_ ->
{error, could_not_find_mapped_port}
end
end.
-spec is_healthy(binary()) -> boolean().
is_healthy(Output) ->
begin
_pipe = gleam@string:trim(Output),
gleam@string:lowercase(_pipe)
end
=:= <<"healthy"/utf8>>.
-spec user_to_arg(gleam@option:option(binary())) -> list(binary()).
user_to_arg(User) ->
case User of
none ->
[];
{some, Value} ->
[<<"-u"/utf8>>, Value]
end.
-spec entrypoint_to_arg(gleam@option:option(binary())) -> list(binary()).
entrypoint_to_arg(Entrypoint) ->
case Entrypoint of
none ->
[];
{some, Value} ->
[<<"--entrypoint"/utf8>>, Value]
end.
-spec working_directory_to_arg(gleam@option:option(binary())) -> list(binary()).
working_directory_to_arg(Working_directory) ->
case Working_directory of
none ->
[];
{some, Value} ->
[<<"-w"/utf8>>, Value]
end.
-spec env_file_to_arg(gleam@option:option(binary())) -> list(binary()).
env_file_to_arg(Env_file) ->
case Env_file of
none ->
[];
{some, File} ->
[<<"--env-file"/utf8>>, File]
end.
-spec health_check_command_to_arg(gleam@option:option(list(binary()))) -> list(binary()).
health_check_command_to_arg(Command) ->
case Command of
none ->
[];
{some, Value} ->
[<<"--health-cmd"/utf8>>, gleam@string:join(Value, <<" "/utf8>>)]
end.
-spec health_check_retries_to_arg(gleam@option:option(integer())) -> list(binary()).
health_check_retries_to_arg(Retries) ->
case Retries of
none ->
[];
{some, Value} ->
[<<"--health-retries"/utf8>>, gleam@int:to_string(Value)]
end.
-spec port_to_arg(port_()) -> list(binary()).
port_to_arg(Port) ->
[<<"-p"/utf8>>,
<<<<<<<<(erlang:element(2, Port))/binary, ":0:"/utf8>>/binary,
(gleam@int:to_string(erlang:element(3, Port)))/binary>>/binary,
"/"/utf8>>/binary,
(begin
_pipe = gleam@string:inspect(erlang:element(4, Port)),
gleam@string:lowercase(_pipe)
end)/binary>>].
-spec env_to_arg(environment_variable()) -> list(binary()).
env_to_arg(Env) ->
[<<"-e"/utf8>>,
<<<<(erlang:element(2, Env))/binary, "="/utf8>>/binary,
(erlang:element(3, Env))/binary>>].
-spec unit_to_string(any()) -> binary().
unit_to_string(Unit) ->
_assert_subject = begin
_pipe = gleam@string:inspect(Unit),
gleam@string:first(_pipe)
end,
{ok, Value} = case _assert_subject of
{ok, _} -> _assert_subject;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Assertion pattern match failed"/utf8>>,
value => _assert_fail,
module => <<"melon/container"/utf8>>,
function => <<"unit_to_string"/utf8>>,
line => 485})
end,
_pipe@1 = Value,
gleam@string:lowercase(_pipe@1).
-spec memory_limit_to_arg(gleam@option:option(memory_limit())) -> list(binary()).
memory_limit_to_arg(Memory_limit) ->
case Memory_limit of
none ->
[];
{some, {memory_limit, Limit, Unit}} ->
[<<"-m"/utf8>>,
<<(gleam@int:to_string(Limit))/binary,
(unit_to_string(Unit))/binary>>]
end.
-spec health_check_interval_to_arg(gleam@option:option(health_check_interval())) -> list(binary()).
health_check_interval_to_arg(Interval) ->
case Interval of
none ->
[];
{some, Value} ->
[<<"--health-interval"/utf8>>,
<<(gleam@int:to_string(erlang:element(2, Value)))/binary,
(unit_to_string(erlang:element(3, Value)))/binary>>]
end.
-spec health_check_timeout_to_arg(gleam@option:option(health_check_timeout())) -> list(binary()).
health_check_timeout_to_arg(Interval) ->
case Interval of
none ->
[];
{some, Value} ->
[<<"--health-timeout"/utf8>>,
<<(gleam@int:to_string(erlang:element(2, Value)))/binary,
(unit_to_string(erlang:element(3, Value)))/binary>>]
end.
-spec health_check_start_period_to_arg(
gleam@option:option(health_check_start_period())
) -> list(binary()).
health_check_start_period_to_arg(Interval) ->
case Interval of
none ->
[];
{some, Value} ->
[<<"--health-start-period"/utf8>>,
<<(gleam@int:to_string(erlang:element(2, Value)))/binary,
(unit_to_string(erlang:element(3, Value)))/binary>>]
end.
-spec start(container()) -> {ok, container()} |
{error, container_start_failure()}.
start(Container) ->
Arguments = begin
_pipe = [<<"run"/utf8>>, <<"--rm"/utf8>>],
_pipe@1 = lists:append(_pipe, user_to_arg(erlang:element(4, Container))),
_pipe@2 = lists:append(
_pipe@1,
entrypoint_to_arg(erlang:element(5, Container))
),
_pipe@3 = lists:append(
_pipe@2,
working_directory_to_arg(erlang:element(7, Container))
),
_pipe@4 = lists:append(
_pipe@3,
memory_limit_to_arg(erlang:element(8, Container))
),
_pipe@5 = lists:append(
_pipe@4,
env_file_to_arg(erlang:element(9, Container))
),
_pipe@6 = lists:append(
_pipe@5,
health_check_command_to_arg(erlang:element(10, Container))
),
_pipe@7 = lists:append(
_pipe@6,
health_check_interval_to_arg(erlang:element(11, Container))
),
_pipe@8 = lists:append(
_pipe@7,
health_check_timeout_to_arg(erlang:element(12, Container))
),
_pipe@9 = lists:append(
_pipe@8,
health_check_start_period_to_arg(erlang:element(13, Container))
),
_pipe@10 = lists:append(
_pipe@9,
health_check_retries_to_arg(erlang:element(14, Container))
),
_pipe@11 = lists:append(
_pipe@10,
gleam@list:flat_map(
erlang:element(15, Container),
fun port_to_arg/1
)
),
_pipe@12 = lists:append(
_pipe@11,
gleam@list:flat_map(erlang:element(16, Container), fun env_to_arg/1)
),
_pipe@13 = lists:append(
_pipe@12,
[<<"-d"/utf8>>, erlang:element(3, Container)]
),
lists:append(
_pipe@13,
gleam@option:unwrap(erlang:element(6, Container), [])
)
end,
case docker_cmd(Arguments) of
{ok, {command_output, 0, Output}} ->
case begin
_pipe@14 = gleam@string:trim(Output),
_pipe@15 = gleam@string:split(_pipe@14, <<"\n"/utf8>>),
gleam@list:last(_pipe@15)
end of
{error, _} ->
_pipe@16 = gleam@string:trim(Output),
_pipe@17 = {could_not_start_container, _pipe@16},
{error, _pipe@17};
{ok, Container_id} ->
_pipe@19 = erlang:setelement(
2,
Container,
begin
_pipe@18 = gleam@string:trim(Container_id),
{some, _pipe@18}
end
),
{ok, _pipe@19}
end;
{ok, {command_output, _, Output@1}} ->
_pipe@20 = gleam@string:trim(Output@1),
_pipe@21 = {could_not_start_container, _pipe@20},
{error, _pipe@21};
{error, Reason} ->
_pipe@22 = gleam@string:trim(Reason),
_pipe@23 = {could_not_start_container, _pipe@22},
{error, _pipe@23}
end.
-spec do_wait_until_healthy(container(), binary(), integer()) -> {ok,
container()} |
{error, container_health_status_failure()}.
do_wait_until_healthy(Container, Container_id, Retries) ->
case Retries =:= -1 of
true ->
{error, container_is_unhealthy};
false ->
case docker_cmd(
[<<"inspect"/utf8>>,
<<"--format"/utf8>>,
<<"{{.State.Health.Status}}"/utf8>>,
Container_id]
) of
{error, Reason} ->
_pipe = gleam@string:trim(Reason),
_pipe@1 = {could_not_obtain_health_status, _pipe},
{error, _pipe@1};
{ok, {command_output, 0, Output}} ->
case is_healthy(Output) of
true ->
{ok, Container};
false ->
timer:sleep(1000),
do_wait_until_healthy(
Container,
Container_id,
Retries - 1
)
end;
{ok, {command_output, _, Output@1}} ->
_pipe@2 = gleam@string:trim(Output@1),
_pipe@3 = {could_not_obtain_health_status, _pipe@2},
{error, _pipe@3}
end
end.
-spec wait_until_healthy(container(), integer()) -> {ok, container()} |
{error, container_health_status_failure()}.
wait_until_healthy(Container, Retries) ->
case erlang:element(2, Container) of
none ->
{error,
cannot_obtain_health_status_of_container_that_is_not_running};
{some, Container_id} ->
case Retries of
_ when Retries < 0 ->
do_wait_until_healthy(Container, Container_id, 0);
_ ->
do_wait_until_healthy(Container, Container_id, Retries)
end
end.