%%%------------------------------------------------------------------- %%% @author anlicheng %%% @copyright (C) 2025, %%% @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 = [<>|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 <- 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)).