329 lines
12 KiB
Erlang
329 lines
12 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @author anlicheng
|
|
%%% @copyright (C) 2025, <COMPANY>
|
|
%%% @doc
|
|
%%%
|
|
%%% @end
|
|
%%% Created : 15. 9月 2025 16:11
|
|
%%%-------------------------------------------------------------------
|
|
-module(efka_docker_command).
|
|
-author("anlicheng").
|
|
|
|
%% API
|
|
-export([pull_image/1, check_image_exist/1]).
|
|
-export([create_container/3, check_container_exist/1, is_container_running/1]).
|
|
|
|
-spec pull_image(Image :: binary()) -> ok | {error, Reason :: any()}.
|
|
pull_image(Image) when is_binary(Image) ->
|
|
%% todo 重定向错误流 {stderr_to_stdout, true}
|
|
PortSettings = [stream, exit_status, use_stdio, binary],
|
|
ExecCmd = "docker pull " ++ binary_to_list(Image),
|
|
lager:debug("cmd : ~p", [ExecCmd]),
|
|
case catch erlang:open_port({spawn, ExecCmd}, PortSettings) of
|
|
Port when is_port(Port) ->
|
|
case gather_output(Port) of
|
|
{0, Output} ->
|
|
lager:debug("docker pull output: ~p", [Output]),
|
|
ok;
|
|
{ExitCode, Error} ->
|
|
lager:debug("call me here: ~p", [Error]),
|
|
{error, {ExitCode, Error}}
|
|
end;
|
|
Error ->
|
|
lager:debug("error: ~p", [Error]),
|
|
{error, <<"exec command startup failed">>}
|
|
end.
|
|
|
|
-spec create_container(ContainerName :: binary(), ContainerDir :: string(), Config :: map()) -> {ok, ContainerId :: binary()} | {error, Reason :: any()}.
|
|
create_container(ContainerName, ContainerDir, Config) when is_binary(ContainerName), is_list(ContainerDir), is_map(Config) ->
|
|
Image = maps:get(<<"image">>, Config),
|
|
Cmd = maps:get(<<"command">>, Config, []),
|
|
|
|
%% 挂载预留的目录,用来作为配置文件的存放
|
|
BinContainerDir = list_to_binary(ContainerDir),
|
|
BaseOptions = [<<"-v">>, <<BinContainerDir/binary, ":/etc/">>],
|
|
|
|
Options = build_options(Config),
|
|
Args = lists:flatten([Image | BaseOptions ++ Options ++ Cmd]),
|
|
CreateArgs = iolist_to_binary(lists:join(<<" ">>, Args)),
|
|
|
|
%% todo 重定向错误流 {stderr_to_stdout, true}
|
|
PortSettings = [stream, exit_status, use_stdio, binary],
|
|
ExecCmd = "docker create --name " ++ binary_to_list(ContainerName) ++ " " ++ binary_to_list(CreateArgs),
|
|
lager:debug("create_container cmd : ~p", [ExecCmd]),
|
|
case catch erlang:open_port({spawn, ExecCmd}, PortSettings) of
|
|
Port when is_port(Port) ->
|
|
case gather_output(Port) of
|
|
{0, <<ContainerId:64/binary, $\n>>} ->
|
|
{ok, ContainerId};
|
|
{ExitCode, Error} ->
|
|
lager:debug("call me here: ~p", [Error]),
|
|
{error, {ExitCode, Error}}
|
|
end;
|
|
Error ->
|
|
lager:debug("error: ~p", [Error]),
|
|
{error, <<"exec command startup failed">>}
|
|
end.
|
|
|
|
-spec is_container_running(ContainerId :: binary()) -> boolean().
|
|
is_container_running(ContainerId) when is_binary(ContainerId) ->
|
|
%% todo 重定向错误流 {stderr_to_stdout, true}
|
|
PortSettings = [stream, exit_status, use_stdio, binary],
|
|
ExecCmd = "docker inspect -f '{{.State.Running}}' " ++ binary_to_list(ContainerId),
|
|
case catch erlang:open_port({spawn, ExecCmd}, PortSettings) of
|
|
Port when is_port(Port) ->
|
|
case gather_output(Port) of
|
|
{0, Val0} ->
|
|
Val = string:trim(Val0),
|
|
Val =:= <<"true">>;
|
|
_ ->
|
|
false
|
|
end;
|
|
_Error ->
|
|
false
|
|
end.
|
|
|
|
-spec check_image_exist(Image :: binary()) -> boolean().
|
|
check_image_exist(Image) when is_binary(Image) ->
|
|
PortSettings = [stream, exit_status, use_stdio, binary],
|
|
ExecCmd = "docker images -q " ++ binary_to_list(Image),
|
|
lager:debug("cmd : ~p", [ExecCmd]),
|
|
case catch erlang:open_port({spawn, ExecCmd}, PortSettings) of
|
|
Port when is_port(Port) ->
|
|
case gather_output(Port) of
|
|
{0, <<>>} ->
|
|
false;
|
|
{0, <<_:12/binary, $\n>>} ->
|
|
true;
|
|
{_ExitCode, _Error} ->
|
|
false
|
|
end;
|
|
_Error ->
|
|
false
|
|
end.
|
|
|
|
-spec check_container_exist(ContainerName :: binary()) -> boolean().
|
|
check_container_exist(ContainerName) when is_binary(ContainerName) ->
|
|
PortSettings = [stream, exit_status, use_stdio, binary],
|
|
ExecCmd = "docker inspect --type=container " ++ binary_to_list(ContainerName) ++ " >/dev/null 2>&1",
|
|
lager:debug("check_container_exist cmd : ~p", [ExecCmd]),
|
|
case catch erlang:open_port({spawn, ExecCmd}, PortSettings) of
|
|
Port when is_port(Port) ->
|
|
case gather_output(Port) of
|
|
{0, _} ->
|
|
true;
|
|
{_ExitCode, _Error} ->
|
|
false
|
|
end;
|
|
_Error ->
|
|
false
|
|
end.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%% helper methods
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
gather_output(Port) ->
|
|
gather_output(Port, <<>>).
|
|
gather_output(Port, Acc) ->
|
|
receive
|
|
{Port, {data, Data}} ->
|
|
gather_output(Port, [Acc, Data]);
|
|
{Port, {exit_status, Status}} ->
|
|
{Status, iolist_to_binary(Acc)}
|
|
end.
|
|
|
|
extract_sha256(Output) when is_binary(Output) ->
|
|
Parts = binary:split(Output, <<$\n>>, [global]),
|
|
lager:debug("parts: ~p", [Parts]),
|
|
case lists:search(fun(Line) -> starts_with(Line, <<"Digest:">>) end, Parts) of
|
|
{value, Digest} ->
|
|
Sha256 = lists:last(binary:split(Digest, <<":">>, [global])),
|
|
{ok, Sha256};
|
|
false ->
|
|
error
|
|
end.
|
|
|
|
starts_with(Binary, Prefix) when is_binary(Binary), is_binary(Prefix) ->
|
|
PrefixSize = byte_size(Prefix),
|
|
case Binary of
|
|
<<Prefix:PrefixSize/binary, _Rest/binary>> -> true;
|
|
_ -> false
|
|
end.
|
|
|
|
%% 构建所有参数
|
|
build_options(Config) ->
|
|
lists:flatten([
|
|
build_entrypoint(Config),
|
|
build_ports(Config),
|
|
build_expose(Config),
|
|
build_volumes(Config),
|
|
build_env(Config),
|
|
build_env_file(Config),
|
|
build_networks(Config),
|
|
build_labels(Config),
|
|
build_restart(Config),
|
|
build_user(Config),
|
|
build_working_dir(Config),
|
|
build_hostname(Config),
|
|
build_privileged(Config),
|
|
build_cap_add_drop(Config),
|
|
build_devices(Config),
|
|
build_memory(Config),
|
|
build_cpu(Config),
|
|
build_ulimits(Config),
|
|
build_sysctls(Config),
|
|
build_tmpfs(Config),
|
|
build_extra_hosts(Config),
|
|
build_healthcheck(Config)
|
|
]).
|
|
|
|
build_entrypoint(Config) ->
|
|
case maps:get(<<"entrypoint">>, Config, []) of
|
|
[] -> [];
|
|
EP -> [<<"--entrypoint">> | EP]
|
|
end.
|
|
|
|
build_ports(Config) ->
|
|
Ports = maps:get(<<"ports">>, Config, []),
|
|
lists:map(fun(P) -> [<<"-p">>, P] end, Ports).
|
|
|
|
build_expose(Config) ->
|
|
Ports = maps:get(<<"expose">>, Config, []),
|
|
lists:map(fun(P) -> [<<"--expose">>, P] end, Ports).
|
|
|
|
build_volumes(Config) ->
|
|
Vols = maps:get(<<"volumes">>, Config, []),
|
|
lists:map(fun(V) -> [<<"-v">>, V] end, Vols).
|
|
|
|
build_env(Config) ->
|
|
Envs = maps:get(<<"envs">>, Config, []),
|
|
lists:map(fun(E) -> [<<"-e">>, E] end, Envs).
|
|
|
|
build_env_file(Config) ->
|
|
Files = maps:get(<<"env_file">>, Config, []),
|
|
lists:map(fun(F) -> [<<"--env-file">>, F] end, Files).
|
|
|
|
build_networks(Config) ->
|
|
Nets = maps:get(<<"networks">>, Config, []),
|
|
lists:map(fun(Net) -> [<<"--network">>, Net] end, Nets).
|
|
|
|
build_labels(Config) ->
|
|
case maps:get(<<"labels">>, Config, #{}) of
|
|
#{} ->
|
|
[];
|
|
Labels ->
|
|
lists:map(fun({K, V}) -> [<<"--label">>, <<K/binary, "=", V/binary>>] end, maps:to_list(Labels))
|
|
end.
|
|
|
|
build_restart(Config) ->
|
|
case maps:get(<<"restart">>, Config, undefined) of
|
|
undefined -> [];
|
|
Policy -> [<<"--restart">>, Policy]
|
|
end.
|
|
|
|
build_user(Config) ->
|
|
case maps:get(<<"user">>, Config, undefined) of
|
|
undefined -> [];
|
|
U -> [<<"--user">>, U]
|
|
end.
|
|
|
|
build_working_dir(Config) ->
|
|
case maps:get(<<"working_dir">>, Config, undefined) of
|
|
undefined -> [];
|
|
D -> [<<"--workdir">>, D]
|
|
end.
|
|
|
|
build_hostname(Config) ->
|
|
case maps:get(<<"hostname">>, Config, undefined) of
|
|
undefined -> [];
|
|
H -> [<<"--hostname">>, H]
|
|
end.
|
|
|
|
build_privileged(Config) ->
|
|
case maps:get(<<"privileged">>, Config, false) of
|
|
true -> [<<"--privileged">>];
|
|
_ -> []
|
|
end.
|
|
|
|
build_cap_add_drop(Config) ->
|
|
Add = maps:get(<<"cap_add">>, Config, []),
|
|
Drop = maps:get(<<"cap_drop">>, Config, []),
|
|
lists:map(fun(C) -> [<<"--cap-add">>, C] end, Add) ++ lists:map(fun(C0) -> [<<"--cap-drop">>, C0] end, Drop).
|
|
|
|
build_devices(Config) ->
|
|
Devs = maps:get(<<"devices">>, Config, []),
|
|
lists:map(fun(D) -> [<<"--device">>, D] end, Devs).
|
|
|
|
build_memory(Config) ->
|
|
Mem = maps:get(<<"mem_limit">>, Config, undefined),
|
|
MemRes = maps:get(<<"mem_reservation">>, Config, undefined),
|
|
Res1 = if Mem /= undefined -> [<<"--memory">>, Mem]; true -> [] end,
|
|
Res2 = if MemRes /= undefined -> [<<"--memory-reservation">>, MemRes]; true -> [] end,
|
|
Res1 ++ Res2.
|
|
|
|
build_cpu(Config) ->
|
|
CPU = maps:get(<<"cpus">>, Config, undefined),
|
|
Shares = maps:get(<<"cpu_shares">>, Config, undefined),
|
|
Res1 = if
|
|
CPU /= undefined ->
|
|
Bin = iolist_to_binary(io_lib:format("~p", [CPU])),
|
|
[<<"--cpus">>, Bin];
|
|
true ->
|
|
[]
|
|
end,
|
|
Res2 = if
|
|
Shares /= undefined ->
|
|
Bin1 = iolist_to_binary(io_lib:format("~p", [Shares])),
|
|
[<<"--cpu-shares">>, Bin1];
|
|
true ->
|
|
[]
|
|
end,
|
|
Res1 ++ Res2.
|
|
|
|
build_ulimits(Config) ->
|
|
UL = maps:get(<<"ulimits">>, Config, #{}),
|
|
lists:map(fun({K, V}) -> [<<"--ulimit">>, <<K/binary, "=", V/binary>>] end, maps:to_list(UL)).
|
|
|
|
build_sysctls(Config) ->
|
|
SC = maps:get(<<"sysctls">>, Config, #{}),
|
|
lists:map(fun({K, V}) -> [<<"--sysctl ">>, <<K/binary, "=", V/binary>>] end, maps:to_list(SC)).
|
|
|
|
build_tmpfs(Config) ->
|
|
Tmp = maps:get(<<"tmpfs">>, Config, []),
|
|
lists:map(fun(T) -> [<<"--tmpfs">>, T] end, Tmp).
|
|
|
|
build_extra_hosts(Config) ->
|
|
Hosts = maps:get(<<"extra_hosts">>, Config, []),
|
|
lists:map(fun(H) -> [<<"--add-host">>, H] end, Hosts).
|
|
|
|
build_healthcheck(Config) ->
|
|
HC = maps:get(<<"healthcheck">>, Config, #{}),
|
|
lists:map(fun({K, V}) ->
|
|
case K of
|
|
<<"test">> ->
|
|
case V of
|
|
%% Test 是 ["CMD-SHELL", Cmd]
|
|
[<<"CMD-SHELL">>, Cmd] ->
|
|
[<<"--health-cmd">>, <<$", Cmd/binary, $">>];
|
|
%% Test 是 ["CMD", Arg1, Arg2...]
|
|
[<<"CMD">> | CmdList] ->
|
|
CmdArgs = iolist_to_binary(lists:join(<<" ">>, CmdList)),
|
|
[<<"--health-cmd">>, <<$", CmdArgs/binary, $">>];
|
|
%% Test 是 <<"NONE">>
|
|
[<<"NONE">>] ->
|
|
[<<"--no-healthcheck">>];
|
|
_ ->
|
|
[]
|
|
end;
|
|
<<"interval">> ->
|
|
[<<"--health-interval">>, V];
|
|
<<"timeout">> ->
|
|
[<<"--health-timeout">>, V];
|
|
<<"retries">> ->
|
|
[<<"--health-retries">>, io_lib:format("~p", [V])];
|
|
_ ->
|
|
[]
|
|
end
|
|
end, maps:to_list(HC)).
|