diff --git a/apps/efka/src/docker/docker_commands.erl b/apps/efka/src/docker/docker_commands.erl index 486fe37..cd01bb4 100644 --- a/apps/efka/src/docker/docker_commands.erl +++ b/apps/efka/src/docker/docker_commands.erl @@ -12,337 +12,456 @@ %% 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]). +-export([test/0, test_create_container/0, test1/0]). + +test() -> + Image = <<"docker.1ms.run/library/nginx:latest">>, + pull_image(Image, fun(Msg) -> lager:debug("msg is: ~p", [Msg]) end). + +test1() -> + Id = <<"redpanda-console">>, + StopRes = stop_container(Id), + lager:debug("stop res: ~p", [StopRes]), + StartRes = start_container(Id), + lager:debug("start res: ~p", [StartRes]). + +test_create_container() -> + M = #{ + <<"image">> => <<"docker.1ms.run/library/nginx:latest">>, + <<"container_name">> => <<"my_nginx_new1">>, + <<"command">> => [ + <<"nginx">>, + <<"-g">>, + <<"daemon off;">> + ], + <<"entrypoint">> => [ + <<"/docker-entrypoint.sh">> + ], + <<"envs">> => [ + <<"ENV1=val1">>, + <<"ENV2=val2">> + ], + <<"env_file">> => [ + <<"./env.list">> + ], + <<"ports">> => [ + <<"8080:80">>, + <<"443:443">> + ], + <<"expose">> => [ + <<"80">>, + <<"443">> + ], + <<"volumes">> => [ + <<"/host/data:/data">>, + <<"/host/log:/var/log">> + ], + <<"networks">> => [ + <<"mynet">> + ], + <<"labels">> => #{ + <<"role">> => <<"web">>, + <<"env">> => <<"prod">> + }, + <<"restart">> => <<"always">>, + <<"user">> => <<"www-data">>, + <<"working_dir">> => <<"/app">>, + <<"hostname">> => <<"myhost">>, + <<"privileged">> => true, + <<"cap_add">> => [ + <<"NET_ADMIN">> + ], + <<"cap_drop">> => [ + <<"MKNOD">> + ], + <<"devices">> => [ + <<"/dev/snd:/dev/snd">> + ], + <<"mem_limit">> => <<"512m">>, + <<"mem_reservation">> => <<"256m">>, + <<"cpu_shares">> => 512, + <<"cpus">> => 1.5, + <<"ulimits">> => #{ + <<"nofile">> => <<"1024:2048">> + }, + <<"sysctls">> => #{ + <<"net.ipv4.ip_forward">> => <<"1">> + }, + <<"tmpfs">> => [ + <<"/tmp">> + ], + <<"extra_hosts">> => [ + <<"host1:192.168.0.1">> + ], + <<"healthcheck">> => #{ + <<"test">> => [ + <<"CMD-SHELL">>, + <<"curl -f http://localhost || exit 1">> + ], + <<"interval">> => <<"30s">>, + <<"timeout">> => <<"10s">>, + <<"retries">> => 3 + } + }, + create_container(<<"my_nginx_xx3">>, "/usr/local/code/efka/", M). -spec pull_image(Image :: binary(), Callback :: fun((Msg :: binary()) -> no_return())) -> ok | {error, ExitCode :: integer()}. pull_image(Image, Callback) when is_binary(Image), is_function(Callback, 1) -> - PortSettings = [stream, exit_status, use_stdio, stderr_to_stdout, 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_pull_output(Port, Callback) of - 0 -> - ok; - ExitCode -> - {error, ExitCode} - end; - Error -> - lager:debug("error: ~p", [Error]), - {error, -1} - end. + Url = lists:flatten(io_lib:format("/images/create?fromImage=~s", [binary_to_list(Image)])), + docker_http:stream_request(Callback, "POST", Url, undefined, []). -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 -> + Url = io_lib:format("/images/~s/json", [binary_to_list(Image)]), + case docker_http:request("GET", Url, undefined, []) of + {ok, 200, _Headers, Resp} -> + M = catch jiffy:decode(Resp, [return_maps]), + is_map(M) andalso maps:is_key(<<"Id">>, M); + {ok, 404, _Headers, _Error} -> 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) -> - Image = maps:get(<<"image">>, Config), - Cmd = maps:get(<<"command">>, Config, []), - + Url = lists:flatten(io_lib:format("/containers/create?name=~s", [binary_to_list(ContainerName)])), %% 挂载预留的目录,用来作为配置文件的存放 BinContainerDir = list_to_binary(docker_container_helper:make_etc_dir_name(ContainerDir)), - BaseOptions = [<<"-v">>, <>], - Options = build_options(Config), - Args = lists:flatten([Image | BaseOptions ++ Options ++ Cmd]), - CreateArgs = iolist_to_binary(lists:join(<<" ">>, Args)), + %% 增加自定义的用来放配置文件的目录 + Volumes0 = maps:get(<<"volumes">>, Config, []), + Volumes = [<>|Volumes0], + NewConfig = Config#{<<"volumes">> => Volumes}, - PortSettings = [stream, exit_status, stderr_to_stdout, use_stdio, binary], - ExecCmd = "docker create --name " ++ binary_to_list(ContainerName) ++ " " ++ binary_to_list(CreateArgs), - lager:debug("create_container cmd : ~p", [ExecCmd]), + Options = build_options(NewConfig), + lists:foreach(fun({K, V}) -> lager:debug("~p => ~p", [K, V]) end, maps:to_list(Options)), - case catch erlang:open_port({spawn, ExecCmd}, PortSettings) of - Port when is_port(Port) -> - case gather_output(Port) of - {0, ContainerId} -> - {ok, string:trim(ContainerId)}; - {ExitCode, Error} -> - {error, {ExitCode, Error}} - end; - Error -> - lager:debug("error: ~p", [Error]), - {error, <<"exec command startup failed">>} + Body = iolist_to_binary(jiffy:encode(Options, [force_utf8])), + Headers = [ + {<<"Content-Type">>, <<"application/json">>} + ], + case docker_http:request("POST", Url, Body, Headers) of + {ok, 201, _Headers, Resp} -> + #{<<"Id">> := ContainerId} = jiffy:decode(Resp, [return_maps]), + {ok, ContainerId}; + {ok, _StatusCode, _, ErrorResp} -> + case catch jiffy:decode(ErrorResp, [return_maps]) of + #{<<"message">> := Msg} -> + {error, Msg}; + _ -> + {error, ErrorResp} + end end. -spec is_container_running(ContainerId :: binary()) -> boolean(). is_container_running(ContainerId) when is_binary(ContainerId) -> - PortSettings = [stream, exit_status, use_stdio, stderr_to_stdout, 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 -> + 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) -> - 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 -> + 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) -> - PortSettings = [stream, exit_status, use_stdio, binary], - ExecCmd = "docker start " ++ binary_to_list(ContainerName) ++ " 2>&1", - case catch erlang:open_port({spawn, ExecCmd}, PortSettings) of - Port when is_port(Port) -> - case gather_output(Port) of - {0, _} -> - ok; - {_ExitCode, Error} -> - {error, Error} - end; - _Error -> - {error, <<"启动失败"/utf8>>} + Url = lists:flatten(io_lib:format("/containers/~s/start", [binary_to_list(ContainerName)])), + Headers = [ + {<<"Content-Type">>, <<"application/json">>} + ], + case docker_http:request("POST", Url, undefined, 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 end. -spec stop_container(ContainerName :: binary()) -> ok | {error, Reason :: binary()}. stop_container(ContainerName) when is_binary(ContainerName) -> - PortSettings = [stream, exit_status, use_stdio, binary], - ExecCmd = "docker stop " ++ binary_to_list(ContainerName) ++ " 2>&1", - lager:debug("[docker_commands] cmd : ~p", [ExecCmd]), - case catch erlang:open_port({spawn, ExecCmd}, PortSettings) of - Port when is_port(Port) -> - case gather_output(Port) of - {0, _} -> - ok; - {_ExitCode, Error} -> - {error, Error} - end; - _Error -> - false + Url = lists:flatten(io_lib:format("/containers/~s/stop", [binary_to_list(ContainerName)])), + Headers = [ + {<<"Content-Type">>, <<"application/json">>} + ], + case docker_http:request("POST", Url, undefined, 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 + 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, undefined, 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 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. +%% 构建最终 JSON Map +build_options(Config) when is_map(Config) -> + #{ + <<"Image">> => maps:get(<<"image">>, Config, <<>>), + <<"Cmd">> => maps:get(<<"command">>, Config, []), + <<"Entrypoint">> => maps:get(<<"entrypoint">>, Config, []), + <<"Env">> => maps:get(<<"envs">>, Config, []), + <<"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_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) + ]) + }. --spec gather_pull_output(Port :: port(), Callback :: fun((Msg :: binary()) -> no_return())) -> ExitCode :: integer(). -gather_pull_output(Port, Callback) -> - receive - {Port, {data, Data}} -> - Callback(Data), - gather_pull_output(Port, Callback); - {Port, {exit_status, Status}} -> - Status - 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). +%% 工具函数 +fold_merge(List) -> + lists:foldl(fun maps:merge/2, #{}, List). +%% --- 构建子字段 --- build_expose(Config) -> Ports = maps:get(<<"expose">>, Config, []), - lists:map(fun(P) -> [<<"--expose">>, P] end, Ports). + case Ports of + [] -> #{}; + _ -> + maps:from_list([{<

>, #{}} || P <- Ports]) + end. build_volumes(Config) -> Vols = maps:get(<<"volumes">>, Config, []), - lists:map(fun(V) -> [<<"-v">>, V] end, Vols). + case Vols of + [] -> + #{}; + _ -> + maps:from_list(lists:map(fun(V) -> + [_Host, Cont] = binary:split(V, <<":">>, []), + {Cont, #{}} + end, Vols)) + end. -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_binds(Config) -> + Vols = maps:get(<<"volumes">>, Config, []), + case Vols of + [] -> + #{}; + _ -> + #{<<"Binds">> => Vols} + end. 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">>, <> => NetCfg} end. + +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 -> [<<"--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] + undefined -> + #{}; + Policy -> + #{<<"RestartPolicy">> => #{<<"Name">> => Policy}} end. build_privileged(Config) -> case maps:get(<<"privileged">>, Config, false) of - true -> [<<"--privileged">>]; - _ -> [] + true -> + #{<<"Privileged">> => true}; + _ -> + #{} 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). + case {Add, Drop} of + {[], []} -> + #{}; + _ -> + #{<<"CapAdd">> => Add, <<"CapDrop">> => Drop} + end. build_devices(Config) -> Devs = maps:get(<<"devices">>, Config, []), - lists:map(fun(D) -> [<<"--device">>, D] end, Devs). + 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), - Res1 = if Mem /= undefined -> [<<"--memory">>, Mem]; true -> [] end, - Res2 = if MemRes /= undefined -> [<<"--memory-reservation">>, MemRes]; true -> [] end, - Res1 ++ Res2. + 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), - 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. + 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, #{}), - lists:map(fun({K, V}) -> [<<"--ulimit">>, <>, 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, #{}), - lists:map(fun({K, V}) -> [<<"--sysctl ">>, <> => SC} + end. build_tmpfs(Config) -> Tmp = maps:get(<<"tmpfs">>, Config, []), - lists:map(fun(T) -> [<<"--tmpfs">>, T] end, Tmp). + case Tmp of + [] -> + #{}; + _ -> + #{<<"Tmpfs">> => maps:from_list([{T, <<>>} || T <- Tmp])} + end. 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)). + case Hosts of + [] -> + #{}; + _ -> + #{<<"ExtraHosts">> => Hosts} + end. \ No newline at end of file diff --git a/apps/efka/src/docker/docker_uds_commands.erl b/apps/efka/src/docker/docker_uds_commands.erl deleted file mode 100644 index 0609634..0000000 --- a/apps/efka/src/docker/docker_uds_commands.erl +++ /dev/null @@ -1,467 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author anlicheng -%%% @copyright (C) 2025, -%%% @doc -%%% -%%% @end -%%% Created : 15. 9月 2025 16:11 -%%%------------------------------------------------------------------- --module(docker_uds_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]). --export([test/0, test_create_container/0, test1/0]). - -test() -> - Image = <<"docker.1ms.run/library/nginx:latest">>, - pull_image(Image, fun(Msg) -> lager:debug("msg is: ~p", [Msg]) end). - -test1() -> - Id = <<"redpanda-console">>, - StopRes = stop_container(Id), - lager:debug("stop res: ~p", [StopRes]), - StartRes = start_container(Id), - lager:debug("start res: ~p", [StartRes]). - -test_create_container() -> - M = #{ - <<"image">> => <<"docker.1ms.run/library/nginx:latest">>, - <<"container_name">> => <<"my_nginx_new1">>, - <<"command">> => [ - <<"nginx">>, - <<"-g">>, - <<"daemon off;">> - ], - <<"entrypoint">> => [ - <<"/docker-entrypoint.sh">> - ], - <<"envs">> => [ - <<"ENV1=val1">>, - <<"ENV2=val2">> - ], - <<"env_file">> => [ - <<"./env.list">> - ], - <<"ports">> => [ - <<"8080:80">>, - <<"443:443">> - ], - <<"expose">> => [ - <<"80">>, - <<"443">> - ], - <<"volumes">> => [ - <<"/host/data:/data">>, - <<"/host/log:/var/log">> - ], - <<"networks">> => [ - <<"mynet">> - ], - <<"labels">> => #{ - <<"role">> => <<"web">>, - <<"env">> => <<"prod">> - }, - <<"restart">> => <<"always">>, - <<"user">> => <<"www-data">>, - <<"working_dir">> => <<"/app">>, - <<"hostname">> => <<"myhost">>, - <<"privileged">> => true, - <<"cap_add">> => [ - <<"NET_ADMIN">> - ], - <<"cap_drop">> => [ - <<"MKNOD">> - ], - <<"devices">> => [ - <<"/dev/snd:/dev/snd">> - ], - <<"mem_limit">> => <<"512m">>, - <<"mem_reservation">> => <<"256m">>, - <<"cpu_shares">> => 512, - <<"cpus">> => 1.5, - <<"ulimits">> => #{ - <<"nofile">> => <<"1024:2048">> - }, - <<"sysctls">> => #{ - <<"net.ipv4.ip_forward">> => <<"1">> - }, - <<"tmpfs">> => [ - <<"/tmp">> - ], - <<"extra_hosts">> => [ - <<"host1:192.168.0.1">> - ], - <<"healthcheck">> => #{ - <<"test">> => [ - <<"CMD-SHELL">>, - <<"curl -f http://localhost || exit 1">> - ], - <<"interval">> => <<"30s">>, - <<"timeout">> => <<"10s">>, - <<"retries">> => 3 - } - }, - create_container(<<"my_nginx_xx3">>, "/usr/local/code/efka/", M). - --spec pull_image(Image :: binary(), Callback :: fun((Msg :: binary()) -> no_return())) -> ok | {error, ExitCode :: integer()}. -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, undefined, []). - --spec check_image_exist(Image :: binary()) -> boolean(). -check_image_exist(Image) when is_binary(Image) -> - Url = io_lib:format("/images/~s/json", [binary_to_list(Image)]), - case docker_http:request("GET", Url, undefined, []) of - {ok, 200, _Headers, Resp} -> - M = catch jiffy:decode(Resp, [return_maps]), - is_map(M) andalso maps:is_key(<<"Id">>, M); - {ok, 404, _Headers, _Error} -> - 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)])), - %% 挂载预留的目录,用来作为配置文件的存放 - BinContainerDir = list_to_binary(docker_container_helper:make_etc_dir_name(ContainerDir)), - - %% 增加自定义的用来放配置文件的目录 - Volumes0 = maps:get(<<"volumes">>, Config, []), - Volumes = [<>|Volumes0], - NewConfig = Config#{<<"volumes">> => Volumes}, - - Options = build_options(NewConfig), - lists:foreach(fun({K, V}) -> lager:debug("~p => ~p", [K, V]) end, maps:to_list(Options)), - - Body = iolist_to_binary(jiffy:encode(Options, [force_utf8])), - Headers = [ - {<<"Content-Type">>, <<"application/json">>} - ], - case docker_http:request("POST", Url, Body, Headers) of - {ok, 201, _Headers, Resp} -> - #{<<"Id">> := ContainerId} = jiffy:decode(Resp, [return_maps]), - {ok, ContainerId}; - {ok, _StatusCode, _, ErrorResp} -> - case catch jiffy:decode(ErrorResp, [return_maps]) of - #{<<"message">> := Msg} -> - {error, Msg}; - _ -> - {error, ErrorResp} - end - 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, undefined, 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 - 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, undefined, 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 - 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, undefined, 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 - end. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%% 构建最终 JSON Map -build_options(Config) when is_map(Config) -> - #{ - <<"Image">> => maps:get(<<"image">>, Config, <<>>), - <<"Cmd">> => maps:get(<<"command">>, Config, []), - <<"Entrypoint">> => maps:get(<<"entrypoint">>, Config, []), - <<"Env">> => maps:get(<<"envs">>, Config, []), - <<"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_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 <- 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. - - -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. \ No newline at end of file