%%%------------------------------------------------------------------- %%% @author anlicheng %%% @copyright (C) 2025, %%% @doc %%% %%% @end %%% Created : 23. 9月 2025 17:23 %%%------------------------------------------------------------------- -module(docker_commands_tests). -author("anlicheng"). -include("message_pb.hrl"). %% API -export([ test_all/0, test_pull/0, test_check_image_exist/0, test_check_image_not_exist/0, test_create_container/0, test_create_container_without_create_options/0, test_create_container_patches_options/0, test_check_container_exist/0, test_check_container_not_exist/0, test_is_container_running/0, test_start_container/0, test_stop_container/0, test_stop_container_with_timeout/0, test_kill_container/0, test_kill_container_with_signal/0, test_remove_container/0, test_remove_container_with_options/0, test_get_containers/0 ]). -export([test/0]). -define(TEST_IMAGE, <<"docker.1ms.run/library/nginx:latest">>). -define(TEST_CMD, [<<"nginx">>, <<"-g">>, <<"daemon off;">>]). test() -> try test_all() catch _:_:Stack -> logger:debug("statck is: ~p", [Stack]) end. -spec test_all() -> ok. test_all() -> %ok = test_pull(), ok = test_check_image_exist(), ok = test_check_image_not_exist(), ok = test_create_container(), ok = test_create_container_without_create_options(), ok = test_create_container_patches_options(), ok = test_check_container_exist(), ok = test_check_container_not_exist(), ok = test_is_container_running(), %ok = test_start_container(), %ok = test_stop_container(), %ok = test_stop_container_with_timeout(), %ok = test_kill_container(), %ok = test_kill_container_with_signal(), %ok = test_remove_container(), %ok = test_remove_container_with_options(), %ok = test_get_containers(), ok. -spec test_pull() -> ok. test_pull() -> {ok, Ref, Pid} = docker_commands:pull_image(?TEST_IMAGE), await_pull(Ref, Pid). -spec await_pull(reference(), pid()) -> ok. await_pull(Ref, Pid) when is_reference(Ref), is_pid(Pid) -> MRef = erlang:monitor(process, Pid), await_pull(Ref, Pid, MRef). -spec await_pull(reference(), pid(), reference()) -> ok. await_pull(Ref, Pid, MRef) -> receive {docker_client, Ref, {response, _Status, _Headers}} -> await_pull(Ref, Pid, MRef); {docker_client, Ref, {data, Data}} -> logger:debug("msg is: ~p", [Data]), await_pull(Ref, Pid, MRef); {docker_client, Ref, done} -> erlang:demonitor(MRef, [flush]), ok; {docker_client, Ref, {error, Reason}} -> erlang:demonitor(MRef, [flush]), error({pull_failed, Reason}); {'DOWN', MRef, process, Pid, Reason} -> error({pull_client_down, Reason}) end. -spec test_check_image_exist() -> ok. test_check_image_exist() -> ok = test_pull(), true = docker_commands:check_image_exist(?TEST_IMAGE), ok. -spec test_check_image_not_exist() -> ok. test_check_image_not_exist() -> false = docker_commands:check_image_exist(<<"docker.1ms.run/library/not-exists-for-efka-tests:latest">>), ok. -spec test_create_container() -> ok. test_create_container() -> Name = test_container_name(<<"create">>), ContainerDir = prepare_container_dir(Name), try ok = test_pull(), {ok, ContainerId} = docker_commands:create_container(ContainerDir, minimal_params(Name)), true = is_binary(ContainerId), ok after cleanup_container(Name) end. -spec test_create_container_without_create_options() -> ok. test_create_container_without_create_options() -> Name = test_container_name(<<"create-default">>), ContainerDir = prepare_container_dir(Name), Options = docker_container_builder:build_options(Name, ContainerDir, undefined), assert_patched_default_options(Name, ContainerDir, Options), ok. -spec test_create_container_patches_options() -> ok. test_create_container_patches_options() -> Name = test_container_name(<<"create-patch">>), ContainerDir = prepare_container_dir(Name), Create = #'DockerCreateOptions'{ config = #'DockerContainerConfig'{ image = ?TEST_IMAGE, cmd = ?TEST_CMD, env = [<<"EXISTING_ENV=1">>], volumes = [<<"/data">>] }, host_config = #'DockerHostConfig'{ binds = [<<"/tmp:/tmp">>] } }, Params = #'ContainerDeployParams'{ container_name = Name, create = Create }, Options = docker_container_builder:build_options(Name, ContainerDir, Create), assert_patched_default_options(Name, ContainerDir, Options), assert_existing_options_preserved(Options), try ok = test_pull(), {ok, _ContainerId} = docker_commands:create_container(ContainerDir, Params), ok after cleanup_container(Name) end. -spec test_check_container_exist() -> ok. test_check_container_exist() -> Name = test_container_name(<<"exist">>), with_created_container(Name, fun(_ContainerDir, _ContainerId) -> true = docker_commands:check_container_exist(Name), ok end). -spec test_check_container_not_exist() -> ok. test_check_container_not_exist() -> false = docker_commands:check_container_exist(test_container_name(<<"missing">>)), ok. -spec test_is_container_running() -> ok. test_is_container_running() -> Name = test_container_name(<<"running">>), with_created_container(Name, fun(_ContainerDir, ContainerId) -> false = docker_commands:is_container_running(ContainerId), ok = docker_commands:start_container(Name), true = docker_commands:is_container_running(ContainerId), ok = docker_commands:stop_container(Name), false = docker_commands:is_container_running(ContainerId), ok end). -spec test_start_container() -> ok. test_start_container() -> Name = test_container_name(<<"start">>), with_created_container(Name, fun(_ContainerDir, ContainerId) -> ok = docker_commands:start_container(Name), true = docker_commands:is_container_running(ContainerId), ok end). -spec test_stop_container() -> ok. test_stop_container() -> Name = test_container_name(<<"stop">>), with_started_container(Name, fun(_ContainerDir, ContainerId) -> ok = docker_commands:stop_container(Name), false = docker_commands:is_container_running(ContainerId), ok end). -spec test_stop_container_with_timeout() -> ok. test_stop_container_with_timeout() -> Name = test_container_name(<<"stop-timeout">>), with_started_container(Name, fun(_ContainerDir, ContainerId) -> ok = docker_commands:stop_container(Name, 1), false = docker_commands:is_container_running(ContainerId), ok end). -spec test_kill_container() -> ok. test_kill_container() -> Name = test_container_name(<<"kill">>), with_started_container(Name, fun(_ContainerDir, ContainerId) -> ok = docker_commands:kill_container(Name), timer:sleep(200), false = docker_commands:is_container_running(ContainerId), ok end). -spec test_kill_container_with_signal() -> ok. test_kill_container_with_signal() -> Name = test_container_name(<<"kill-signal">>), with_started_container(Name, fun(_ContainerDir, ContainerId) -> ok = docker_commands:kill_container(Name, <<"SIGKILL">>), timer:sleep(200), false = docker_commands:is_container_running(ContainerId), ok end). -spec test_remove_container() -> ok. test_remove_container() -> Name = test_container_name(<<"remove">>), with_created_container(Name, fun(_ContainerDir, _ContainerId) -> ok = docker_commands:remove_container(Name), false = docker_commands:check_container_exist(Name), ok end, false). -spec test_remove_container_with_options() -> ok. test_remove_container_with_options() -> Name = test_container_name(<<"remove-opts">>), with_started_container(Name, fun(_ContainerDir, _ContainerId) -> ok = docker_commands:remove_container(Name, true, false), false = docker_commands:check_container_exist(Name), ok end, false). -spec test_get_containers() -> ok. test_get_containers() -> Name = test_container_name(<<"list">>), with_created_container(Name, fun(_ContainerDir, ContainerId) -> {ok, Containers} = docker_commands:get_containers(), true = is_list(Containers), true = contains_container(Name, ContainerId, Containers), ok end). -spec with_created_container(binary(), fun((string(), binary()) -> ok)) -> ok. with_created_container(Name, Fun) -> with_created_container(Name, Fun, true). -spec with_created_container(binary(), fun((string(), binary()) -> ok), boolean()) -> ok. with_created_container(Name, Fun, Cleanup) when is_binary(Name), is_function(Fun, 2), is_boolean(Cleanup) -> ContainerDir = prepare_container_dir(Name), try ok = test_pull(), {ok, ContainerId} = docker_commands:create_container(ContainerDir, minimal_params(Name)), ok = Fun(ContainerDir, ContainerId) after case Cleanup of true -> cleanup_container(Name); false -> ok end end. -spec with_started_container(binary(), fun((string(), binary()) -> ok)) -> ok. with_started_container(Name, Fun) -> with_started_container(Name, Fun, true). -spec with_started_container(binary(), fun((string(), binary()) -> ok), boolean()) -> ok. with_started_container(Name, Fun, Cleanup) when is_binary(Name), is_function(Fun, 2), is_boolean(Cleanup) -> with_created_container(Name, fun(ContainerDir, ContainerId) -> ok = docker_commands:start_container(Name), ok = Fun(ContainerDir, ContainerId) end, Cleanup). -spec minimal_params(binary()) -> message_pb:'ContainerDeployParams'(). minimal_params(Name) when is_binary(Name) -> #'ContainerDeployParams'{ container_name = Name, create = #'DockerCreateOptions'{ config = #'DockerContainerConfig'{ image = ?TEST_IMAGE, cmd = ?TEST_CMD } } }. -spec prepare_container_dir(binary()) -> string(). prepare_container_dir(Name) when is_binary(Name) -> Dir = lists:flatten(io_lib:format("/tmp/efka_docker_tests/~ts/", [Name])), ok = filelib:ensure_dir(Dir ++ "placeholder"), ok = file:write_file(Dir ++ "service.conf", <<>>, [write]), Dir. -spec cleanup_container(binary()) -> ok. cleanup_container(Name) when is_binary(Name) -> _ = docker_commands:remove_container(Name, true, false), ok. -spec assert_patched_default_options(binary(), string(), map()) -> ok. assert_patched_default_options(Name, ContainerDir, Options) when is_binary(Name), is_list(ContainerDir), is_map(Options) -> ConfigFile = list_to_binary(docker_helper:get_config_file(ContainerDir)), ExpectedBind = <>, #{<<"Env">> := Env, <<"Volumes">> := Volumes, <<"HostConfig">> := #{<<"Binds">> := Binds}} = Options, true = lists:member(<<"CONTAINER_NAME=", Name/binary>>, Env), true = maps:is_key(<<"/usr/local/etc/service.conf">>, Volumes), true = lists:member(ExpectedBind, Binds), ok. -spec assert_existing_options_preserved(map()) -> ok. assert_existing_options_preserved(Options) when is_map(Options) -> #{<<"Env">> := Env, <<"Volumes">> := Volumes, <<"HostConfig">> := #{<<"Binds">> := Binds}} = Options, true = lists:member(<<"EXISTING_ENV=1">>, Env), true = maps:is_key(<<"/data">>, Volumes), true = lists:member(<<"/tmp:/tmp">>, Binds), ok. -spec contains_container(binary(), binary(), [map()]) -> boolean(). contains_container(Name, ContainerId, Containers) when is_binary(Name), is_binary(ContainerId), is_list(Containers) -> lists:any(fun(Container) -> container_matches(Name, ContainerId, Container) end, Containers). -spec container_matches(binary(), binary(), map()) -> boolean(). container_matches(Name, ContainerId, #{<<"Id">> := Id, <<"Names">> := Names}) when is_binary(Id), is_list(Names) -> lists:member(<<"/", Name/binary>>, Names) orelse has_id_prefix(ContainerId, Id); container_matches(_Name, _ContainerId, _Container) -> false. -spec has_id_prefix(binary(), binary()) -> boolean(). has_id_prefix(ExpectedId, ActualId) when is_binary(ExpectedId), is_binary(ActualId) -> PrefixLen = erlang:min(byte_size(ExpectedId), byte_size(ActualId)), binary:part(ExpectedId, 0, PrefixLen) =:= binary:part(ActualId, 0, PrefixLen). -spec test_container_name(binary()) -> binary(). test_container_name(Prefix) when is_binary(Prefix) -> Suffix = integer_to_binary(erlang:unique_integer([positive])), <<"efka-test-", Prefix/binary, "-", Suffix/binary>>.