467 lines
16 KiB
Erlang
467 lines
16 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @author anlicheng
|
|
%%% @copyright (C) 2025, <COMPANY>
|
|
%%% @doc
|
|
%%%
|
|
%%% @end
|
|
%%% Created : 15. 9月 2025 16:11
|
|
%%%-------------------------------------------------------------------
|
|
-module(docker_commands).
|
|
-author("anlicheng").
|
|
|
|
%% API
|
|
-export([pull_image/2, check_image_exist/1]).
|
|
-export([create_container/3, check_container_exist/1, is_container_running/1,
|
|
start_container/1, stop_container/1, remove_container/1, kill_container/1,
|
|
get_containers/0]).
|
|
|
|
-spec pull_image(Image :: binary(), Callback :: fun((Msg :: any()) -> no_return())) -> ok | {error, Reason :: any()}.
|
|
pull_image(Image, Callback) when is_binary(Image), is_function(Callback, 1) ->
|
|
Url = lists:flatten(io_lib:format("/images/create?fromImage=~s", [binary_to_list(Image)])),
|
|
docker_http:stream_request(Callback, "POST", Url, <<>>, []).
|
|
|
|
-spec check_image_exist(Image :: binary()) -> boolean().
|
|
check_image_exist(Image) when is_binary(Image) ->
|
|
Url = lists:flatten(io_lib:format("/images/~s/json", [binary_to_list(Image)])),
|
|
case docker_http:request("GET", Url, <<"">>, []) of
|
|
{ok, 200, _Headers, Resp} ->
|
|
M = catch jiffy:decode(Resp, [return_maps]),
|
|
is_map(M) andalso maps:is_key(<<"Id">>, M);
|
|
_ ->
|
|
false
|
|
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) ->
|
|
Url = lists:flatten(io_lib:format("/containers/create?name=~s", [binary_to_list(ContainerName)])),
|
|
%% 挂载预留的目录,用来作为配置文件的存放
|
|
ConfigFile = list_to_binary(docker_helper:get_config_file(ContainerDir)),
|
|
|
|
%% 增加自定义的用来放配置文件的目录
|
|
Volumes0 = maps:get(<<"volumes">>, Config, []),
|
|
Volumes = [<<ConfigFile/binary, ":/usr/local/etc/service.conf">>|Volumes0],
|
|
NewConfig = Config#{<<"volumes">> => Volumes},
|
|
|
|
Options = build_options(ContainerName, NewConfig),
|
|
display_options(Options),
|
|
|
|
Body = iolist_to_binary(jiffy:encode(Options, [force_utf8])),
|
|
true = is_binary(Body),
|
|
|
|
Headers = [
|
|
{<<"Content-Type">>, <<"application/json">>}
|
|
],
|
|
case docker_http:request("POST", Url, Body, Headers) of
|
|
{ok, 201, _Headers, Resp} ->
|
|
case catch jiffy:decode(Resp, [return_maps]) of
|
|
#{<<"Id">> := ContainerId} when is_binary(ContainerId) ->
|
|
{ok, ContainerId};
|
|
_ ->
|
|
{error, Resp}
|
|
end;
|
|
{ok, _StatusCode, _, ErrorResp} ->
|
|
case catch jiffy:decode(ErrorResp, [return_maps]) of
|
|
#{<<"message">> := Msg} ->
|
|
{error, Msg};
|
|
_ ->
|
|
{error, ErrorResp}
|
|
end;
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end.
|
|
|
|
-spec is_container_running(ContainerId :: binary()) -> boolean().
|
|
is_container_running(ContainerId) when is_binary(ContainerId) ->
|
|
case inspect_container(ContainerId) of
|
|
{ok, #{<<"State">> := #{<<"Running">> := Running}}} ->
|
|
Running;
|
|
{error, _} ->
|
|
false
|
|
end.
|
|
|
|
-spec check_container_exist(ContainerName :: binary()) -> boolean().
|
|
check_container_exist(ContainerName) when is_binary(ContainerName) ->
|
|
case inspect_container(ContainerName) of
|
|
{ok, #{<<"Id">> := Id}} when is_binary(Id) ->
|
|
true;
|
|
_ ->
|
|
false
|
|
end.
|
|
|
|
-spec start_container(ContainerName :: binary()) -> ok | {error, Reason :: binary()}.
|
|
start_container(ContainerName) when is_binary(ContainerName) ->
|
|
Url = lists:flatten(io_lib:format("/containers/~s/start", [binary_to_list(ContainerName)])),
|
|
Headers = [
|
|
{<<"Content-Type">>, <<"application/json">>}
|
|
],
|
|
case docker_http:request("POST", Url, <<>>, Headers) of
|
|
{ok, 204, _Headers, _} ->
|
|
ok;
|
|
{ok, 304, _Headers, _} ->
|
|
{error, <<"container already started">>};
|
|
{ok, _StatusCode, _Header, ErrorResp} ->
|
|
case catch jiffy:decode(ErrorResp) of
|
|
#{<<"message">> := Msg} ->
|
|
{error, Msg};
|
|
_ ->
|
|
{error, ErrorResp}
|
|
end;
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end.
|
|
|
|
-spec stop_container(ContainerName :: binary()) -> ok | {error, Reason :: binary()}.
|
|
stop_container(ContainerName) when is_binary(ContainerName) ->
|
|
Url = lists:flatten(io_lib:format("/containers/~s/stop", [binary_to_list(ContainerName)])),
|
|
Headers = [
|
|
{<<"Content-Type">>, <<"application/json">>}
|
|
],
|
|
case docker_http:request("POST", Url, <<>>, Headers) of
|
|
{ok, 204, _Headers, _} ->
|
|
ok;
|
|
{ok, 304, _Headers, _} ->
|
|
{error, <<"container already stopped">>};
|
|
{ok, _StatusCode, _Header, ErrorResp} ->
|
|
case catch jiffy:decode(ErrorResp) of
|
|
#{<<"message">> := Msg} ->
|
|
{error, Msg};
|
|
_ ->
|
|
{error, ErrorResp}
|
|
end;
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end.
|
|
|
|
-spec kill_container(ContainerName :: binary()) -> ok | {error, Reason :: binary()}.
|
|
kill_container(ContainerName) when is_binary(ContainerName) ->
|
|
Url = lists:flatten(io_lib:format("/containers/~s/kill", [binary_to_list(ContainerName)])),
|
|
Headers = [
|
|
{<<"Content-Type">>, <<"application/json">>}
|
|
],
|
|
case docker_http:request("POST", Url, <<>>, Headers) of
|
|
{ok, 204, _Headers, _} ->
|
|
ok;
|
|
{ok, _StatusCode, _Header, ErrorResp} ->
|
|
case catch jiffy:decode(ErrorResp) of
|
|
#{<<"message">> := Msg} ->
|
|
{error, Msg};
|
|
_ ->
|
|
{error, ErrorResp}
|
|
end;
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end.
|
|
|
|
-spec remove_container(ContainerName :: binary()) -> ok | {error, Reason :: binary()}.
|
|
remove_container(ContainerName) when is_binary(ContainerName) ->
|
|
Url = lists:flatten(io_lib:format("/containers/~s", [binary_to_list(ContainerName)])),
|
|
Headers = [
|
|
{<<"Content-Type">>, <<"application/json">>}
|
|
],
|
|
case docker_http:request("DELETE", Url, <<>>, Headers) of
|
|
{ok, 204, _Headers, _} ->
|
|
ok;
|
|
{ok, 304, _Headers, _} ->
|
|
{error, <<"container already stopped">>};
|
|
{ok, _StatusCode, _Header, ErrorResp} ->
|
|
case catch jiffy:decode(ErrorResp) of
|
|
#{<<"message">> := Msg} ->
|
|
{error, Msg};
|
|
_ ->
|
|
{error, ErrorResp}
|
|
end;
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end.
|
|
|
|
-spec get_containers() -> {ok, Containers :: [map()]} | {error, Reason :: binary()}.
|
|
get_containers() ->
|
|
Url = "/containers/json?all=true",
|
|
Headers = [
|
|
{<<"Content-Type">>, <<"application/json">>}
|
|
],
|
|
case docker_http:request("GET", Url, <<>>, Headers) of
|
|
{ok, 200, _Headers, ContainersBin} ->
|
|
Containers = jiffy:decode(ContainersBin, [return_maps]),
|
|
{ok, Containers};
|
|
{ok, _StatusCode, _Header, ErrorResp} ->
|
|
case catch jiffy:decode(ErrorResp) of
|
|
#{<<"message">> := Msg} ->
|
|
{error, Msg};
|
|
_ ->
|
|
{error, ErrorResp}
|
|
end;
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end.
|
|
|
|
-spec inspect_container(ContainerId :: binary()) -> {ok, Json :: map()} | {error, Error :: any()}.
|
|
inspect_container(ContainerId) when is_binary(ContainerId) ->
|
|
Url = lists:flatten(io_lib:format("/containers/~s/json", [binary_to_list(ContainerId)])),
|
|
Headers = [
|
|
{<<"Content-Type">>, <<"application/json">>}
|
|
],
|
|
case docker_http:request("GET", Url, <<>>, Headers) of
|
|
{ok, 200, _Headers, Resp} ->
|
|
Json = jiffy:decode(Resp, [return_maps]),
|
|
{ok, Json};
|
|
{ok, _StatusCode, _Header, ErrorResp} ->
|
|
case catch jiffy:decode(ErrorResp) of
|
|
#{<<"message">> := Msg} ->
|
|
{error, Msg};
|
|
_ ->
|
|
{error, ErrorResp}
|
|
end;
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%% helper methods
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
%% 构建最终 JSON Map
|
|
build_options(ContainerName, Config) when is_binary(ContainerName), is_map(Config) ->
|
|
%% 容器名称作为环境变量,注入到运行时环境中
|
|
Envs0 = maps:get(<<"envs">>, Config, []),
|
|
Envs = [<<"CONTAINER_NAME=", ContainerName/binary>>|Envs0],
|
|
#{
|
|
<<"Image">> => maps:get(<<"image">>, Config, <<>>),
|
|
<<"Cmd">> => maps:get(<<"command">>, Config, []),
|
|
<<"Entrypoint">> => maps:get(<<"entrypoint">>, Config, []),
|
|
<<"Env">> => Envs,
|
|
<<"Labels">> => maps:get(<<"labels">>, Config, #{}),
|
|
<<"Volumes">> => build_volumes(Config),
|
|
<<"User">> => maps:get(<<"user">>, Config, <<>>),
|
|
<<"WorkingDir">> => maps:get(<<"working_dir">>, Config, <<>>),
|
|
<<"Hostname">> => maps:get(<<"hostname">>, Config, <<>>),
|
|
<<"ExposedPorts">> => build_expose(Config),
|
|
<<"NetworkingConfig">> => build_networks(Config),
|
|
<<"Healthcheck">> => build_healthcheck(Config),
|
|
<<"HostConfig">> => fold_merge([
|
|
build_network_mode(Config),
|
|
build_binds(Config),
|
|
build_restart(Config),
|
|
build_privileged(Config),
|
|
build_cap_add_drop(Config),
|
|
build_devices(Config),
|
|
build_memory(Config),
|
|
build_cpu(Config),
|
|
build_ulimits(Config),
|
|
build_tmpfs(Config),
|
|
build_sysctls(Config),
|
|
build_extra_hosts(Config)
|
|
])
|
|
}.
|
|
|
|
%% 工具函数
|
|
fold_merge(List) ->
|
|
lists:foldl(fun maps:merge/2, #{}, List).
|
|
|
|
%% --- 构建子字段 ---
|
|
build_expose(Config) ->
|
|
Ports = maps:get(<<"expose">>, Config, []),
|
|
case Ports of
|
|
[] -> #{};
|
|
_ ->
|
|
maps:from_list([{<<P/binary, "/tcp">>, #{}} || P <- Ports])
|
|
end.
|
|
|
|
build_volumes(Config) ->
|
|
Vols = maps:get(<<"volumes">>, Config, []),
|
|
case Vols of
|
|
[] ->
|
|
#{};
|
|
_ ->
|
|
maps:from_list(lists:map(fun(V) ->
|
|
[_Host, Cont] = binary:split(V, <<":">>, []),
|
|
{Cont, #{}}
|
|
end, Vols))
|
|
end.
|
|
|
|
build_binds(Config) ->
|
|
Vols = maps:get(<<"volumes">>, Config, []),
|
|
case Vols of
|
|
[] ->
|
|
#{};
|
|
_ ->
|
|
#{<<"Binds">> => Vols}
|
|
end.
|
|
|
|
build_networks(Config) ->
|
|
Nets = maps:get(<<"networks">>, Config, []),
|
|
case Nets of
|
|
[] -> #{};
|
|
_ ->
|
|
NetCfg = maps:from_list([{N, #{}} || N <- Nets]),
|
|
#{<<"EndpointsConfig">> => NetCfg}
|
|
end.
|
|
|
|
build_network_mode(Config) ->
|
|
NetworkMode = maps:get(<<"network_mode">>, Config, <<"bridge">>),
|
|
#{<<"NetworkMode">> => NetworkMode}.
|
|
|
|
parse_mem(Val) ->
|
|
case binary:last(Val) of
|
|
$m ->
|
|
N = binary:part(Val, {0, byte_size(Val)-1}),
|
|
list_to_integer(binary_to_list(N)) * 1024 * 1024;
|
|
$g ->
|
|
N = binary:part(Val, {0, byte_size(Val)-1}),
|
|
list_to_integer(binary_to_list(N)) * 1024 * 1024 * 1024;
|
|
_ ->
|
|
list_to_integer(binary_to_list(Val))
|
|
end.
|
|
|
|
build_healthcheck(Config) ->
|
|
HC = maps:get(<<"healthcheck">>, Config, #{}),
|
|
case maps:size(HC) of
|
|
0 ->
|
|
#{};
|
|
_ ->
|
|
#{
|
|
<<"Test">> => maps:get(<<"test">>, HC, []),
|
|
<<"Interval">> => parse_duration(maps:get(<<"interval">>, HC, <<"0s">>)),
|
|
<<"Timeout">> => parse_duration(maps:get(<<"timeout">>, HC, <<"0s">>)),
|
|
<<"Retries">> => maps:get(<<"retries">>, HC, 0)
|
|
}
|
|
end.
|
|
|
|
parse_duration(Bin) ->
|
|
%% "30s" -> 30000000000
|
|
Sz = byte_size(Bin),
|
|
NBin = binary:part(Bin, {0, Sz-1}),
|
|
N = list_to_integer(binary_to_list(NBin)),
|
|
case binary:last(Bin) of
|
|
$s ->
|
|
N * 1000000000;
|
|
$m ->
|
|
N * 60000000000;
|
|
_ ->
|
|
N
|
|
end.
|
|
|
|
%% --- 构建子字段 ---
|
|
|
|
build_restart(Config) ->
|
|
case maps:get(<<"restart">>, Config, undefined) of
|
|
undefined ->
|
|
#{};
|
|
Policy ->
|
|
#{<<"RestartPolicy">> => #{<<"Name">> => Policy}}
|
|
end.
|
|
|
|
build_privileged(Config) ->
|
|
case maps:get(<<"privileged">>, Config, false) of
|
|
true ->
|
|
#{<<"Privileged">> => true};
|
|
_ ->
|
|
#{}
|
|
end.
|
|
|
|
build_cap_add_drop(Config) ->
|
|
Add = maps:get(<<"cap_add">>, Config, []),
|
|
Drop = maps:get(<<"cap_drop">>, Config, []),
|
|
case {Add, Drop} of
|
|
{[], []} ->
|
|
#{};
|
|
_ ->
|
|
#{<<"CapAdd">> => Add, <<"CapDrop">> => Drop}
|
|
end.
|
|
|
|
build_devices(Config) ->
|
|
Devs = maps:get(<<"devices">>, Config, []),
|
|
case Devs of
|
|
[] ->
|
|
#{};
|
|
_ ->
|
|
DevObjs = [#{<<"PathOnHost">> => H, <<"PathInContainer">> => C,
|
|
<<"CgroupPermissions">> => <<"rwm">>}
|
|
|| D <- Devs,
|
|
[H, C] <- [binary:split(D, <<":">>, [])]],
|
|
#{<<"Devices">> => DevObjs}
|
|
end.
|
|
|
|
build_memory(Config) ->
|
|
Mem = maps:get(<<"mem_limit">>, Config, undefined),
|
|
MemRes = maps:get(<<"mem_reservation">>, Config, undefined),
|
|
HCfg = #{},
|
|
HCfg1 = if
|
|
Mem /= undefined ->
|
|
maps:put(<<"Memory">>, parse_mem(Mem), HCfg);
|
|
true ->
|
|
HCfg
|
|
end,
|
|
if
|
|
MemRes /= undefined ->
|
|
maps:put(<<"MemoryReservation">>, parse_mem(MemRes), HCfg1);
|
|
true ->
|
|
HCfg1
|
|
end.
|
|
|
|
|
|
build_cpu(Config) ->
|
|
CPU = maps:get(<<"cpus">>, Config, undefined),
|
|
Shares = maps:get(<<"cpu_shares">>, Config, undefined),
|
|
HCfg = #{},
|
|
HCfg1 = if
|
|
CPU /= undefined ->
|
|
maps:put(<<"NanoCpus">>, trunc(CPU * 1000000000), HCfg);
|
|
true ->
|
|
HCfg
|
|
end,
|
|
if
|
|
Shares /= undefined ->
|
|
maps:put(<<"CpuShares">>, Shares, HCfg1);
|
|
true ->
|
|
HCfg1
|
|
end.
|
|
|
|
build_ulimits(Config) ->
|
|
UL = maps:get(<<"ulimits">>, Config, #{}),
|
|
case maps:size(UL) of
|
|
0 ->
|
|
#{};
|
|
_ ->
|
|
ULList = lists:map(fun({K, V}) ->
|
|
[S1, H1] = binary:split(V, <<":">>, []),
|
|
S = list_to_integer(binary_to_list(S1)),
|
|
H = list_to_integer(binary_to_list(H1)),
|
|
#{<<"Name">> => K, <<"Soft">> => S, <<"Hard">> => H}
|
|
end, maps:to_list(UL)),
|
|
|
|
#{<<"Ulimits">> => ULList}
|
|
end.
|
|
|
|
build_sysctls(Config) ->
|
|
SC = maps:get(<<"sysctls">>, Config, #{}),
|
|
case maps:size(SC) of
|
|
0 ->
|
|
#{};
|
|
_ ->
|
|
#{<<"Sysctls">> => SC}
|
|
end.
|
|
|
|
build_tmpfs(Config) ->
|
|
Tmp = maps:get(<<"tmpfs">>, Config, []),
|
|
case Tmp of
|
|
[] ->
|
|
#{};
|
|
_ ->
|
|
#{<<"Tmpfs">> => maps:from_list([{T, <<>>} || T <- Tmp])}
|
|
end.
|
|
|
|
build_extra_hosts(Config) ->
|
|
Hosts = maps:get(<<"extra_hosts">>, Config, []),
|
|
case Hosts of
|
|
[] ->
|
|
#{};
|
|
_ ->
|
|
#{<<"ExtraHosts">> => Hosts}
|
|
end.
|
|
|
|
-spec display_options(Options :: map()) -> no_return().
|
|
display_options(Options) when is_map(Options) ->
|
|
logger:debug("deploy options: ~p", [jiffy:encode(Options, [force_utf8])]),
|
|
lists:foreach(fun({K, V}) -> logger:debug("~p => ~p", [K, V]) end, maps:to_list(Options)).
|