%%%------------------------------------------------------------------- %%% @author anlicheng %%% @copyright (C) 2025, %%% @doc %%% %%% @end %%% Created : 15. 9月 2025 16:11 %%%------------------------------------------------------------------- -module(docker_commands). -author("anlicheng"). -include("message_pb.hrl"). %% API -export([pull_image/1, check_image_exist/1]). -export([create_container/2, check_container_exist/1, is_container_running/1, start_container/1, stop_container/1, stop_container/2, remove_container/1, remove_container/3, kill_container/1, kill_container/2, get_containers/0]). -spec pull_image(Image :: binary()) -> {ok, Ref :: reference(), Pid :: pid(), MRef :: reference()} | {error, Reason :: any()}. pull_image(Image) when is_binary(Image) -> Url = lists:flatten(io_lib:format("/images/create?fromImage=~s", [binary_to_list(Image)])), docker_client:start_stream(self(), "POST", Url, <<>>, []). -spec check_image_exist(Image :: binary()) -> boolean(). check_image_exist(Image) when is_binary(Image) -> EncodedImage = uri_string:quote(Image), Url = lists:flatten(io_lib:format("/images/~s/json", [binary_to_list(EncodedImage)])), case docker_client:request("GET", Url, <<"">>, []) of {ok, 200, _Headers, _Resp} -> true; {ok, 404, _, _} -> false; _ -> false end. -spec create_container(ContainerDir :: string(), Params :: message_pb:'ContainerDeployParams'()) -> {ok, ContainerId :: binary()} | {error, Reason :: any()}. create_container(ContainerDir, #'ContainerDeployParams'{container_name = ContainerName, create = CreateOpts }) when is_binary(ContainerName), is_list(ContainerDir) -> Url = lists:flatten(io_lib:format("/containers/create?name=~s", [binary_to_list(ContainerName)])), Options = docker_container_builder:build_options(ContainerName, ContainerDir, CreateOpts), display_options(Options), Body = iolist_to_binary(jiffy:encode(Options, [force_utf8])), true = is_binary(Body), Headers = [ {<<"Content-Type">>, <<"application/json">>} ], case docker_client: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_client: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, [return_maps]) 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) -> stop_container(ContainerName, 0). -spec stop_container(ContainerName :: binary(), TimeoutSeconds :: non_neg_integer()) -> ok | {error, Reason :: binary()}. stop_container(ContainerName, TimeoutSeconds) when is_binary(ContainerName), is_integer(TimeoutSeconds), TimeoutSeconds >= 0 -> Url = build_stop_container_url(ContainerName, TimeoutSeconds), Headers = [ {<<"Content-Type">>, <<"application/json">>} ], case docker_client: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, [return_maps]) 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) -> kill_container(ContainerName, <<>>). -spec kill_container(ContainerName :: binary(), Signal :: binary()) -> ok | {error, Reason :: binary()}. kill_container(ContainerName, Signal) when is_binary(ContainerName), is_binary(Signal) -> Url = build_kill_container_url(ContainerName, Signal), Headers = [ {<<"Content-Type">>, <<"application/json">>} ], case docker_client:request("POST", Url, <<>>, Headers) of {ok, 204, _Headers, _} -> ok; {ok, _StatusCode, _Header, ErrorResp} -> case catch jiffy:decode(ErrorResp, [return_maps]) 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) -> remove_container(ContainerName, false, false). -spec remove_container(ContainerName :: binary(), Force :: boolean(), RemoveVolumes :: boolean()) -> ok | {error, Reason :: binary()}. remove_container(ContainerName, Force, RemoveVolumes) when is_binary(ContainerName), is_boolean(Force), is_boolean(RemoveVolumes) -> Url = build_remove_container_url(ContainerName, Force, RemoveVolumes), Headers = [ {<<"Content-Type">>, <<"application/json">>} ], case docker_client: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, [return_maps]) 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_client: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, [return_maps]) 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_client:request("GET", Url, <<>>, Headers) of {ok, 200, _Headers, Resp} -> decode_container_inspect_summary(Resp); {ok, _StatusCode, _Header, ErrorResp} -> case catch jiffy:decode(ErrorResp, [return_maps]) of #{<<"message">> := Msg} -> {error, Msg}; _ -> {error, ErrorResp} end; {error, Reason} -> {error, Reason} end. -spec decode_container_inspect_summary(binary()) -> {ok, map()} | {error, binary()}. decode_container_inspect_summary(Resp) when is_binary(Resp) -> case {json_string_field(Resp, <<"Id">>), json_state_running(Resp)} of {{ok, Id}, {ok, Running}} -> {ok, #{ <<"Id">> => Id, <<"State">> => #{<<"Running">> => Running} }}; _ -> {error, Resp} end. -spec json_string_field(binary(), binary()) -> {ok, binary()} | error. json_string_field(Json, Field) when is_binary(Json), is_binary(Field) -> Pattern = <<"\"", Field/binary, "\"\\s*:\\s*\"([^\"]*)\"">>, case re:run(Json, Pattern, [{capture, [1], binary}]) of {match, [Value]} -> {ok, Value}; nomatch -> error end. -spec json_state_running(binary()) -> {ok, boolean()} | error. json_state_running(Json) when is_binary(Json) -> Pattern = <<"\"State\"\\s*:\\s*\\{[^}]*\"Running\"\\s*:\\s*(true|false)">>, case re:run(Json, Pattern, [{capture, [1], binary}]) of {match, [<<"true">>]} -> {ok, true}; {match, [<<"false">>]} -> {ok, false}; nomatch -> error 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)). -spec build_stop_container_url(ContainerName :: binary(), TimeoutSeconds :: non_neg_integer()) -> string(). build_stop_container_url(ContainerName, 0) -> lists:flatten(io_lib:format("/containers/~s/stop", [binary_to_list(ContainerName)])); build_stop_container_url(ContainerName, TimeoutSeconds) -> lists:flatten(io_lib:format("/containers/~s/stop?t=~B", [binary_to_list(ContainerName), TimeoutSeconds])). -spec build_kill_container_url(ContainerName :: binary(), Signal :: binary()) -> string(). build_kill_container_url(ContainerName, <<>>) -> lists:flatten(io_lib:format("/containers/~s/kill", [binary_to_list(ContainerName)])); build_kill_container_url(ContainerName, Signal) -> lists:flatten(io_lib:format("/containers/~s/kill?signal=~s", [binary_to_list(ContainerName), binary_to_list(Signal)])). -spec build_remove_container_url(ContainerName :: binary(), Force :: boolean(), RemoveVolumes :: boolean()) -> string(). build_remove_container_url(ContainerName, Force, RemoveVolumes) -> ForceValue = boolean_to_query_value(Force), RemoveVolumesValue = boolean_to_query_value(RemoveVolumes), lists:flatten(io_lib:format("/containers/~s?force=~s&v=~s", [ binary_to_list(ContainerName), ForceValue, RemoveVolumesValue ])). -spec boolean_to_query_value(boolean()) -> string(). boolean_to_query_value(true) -> "true"; boolean_to_query_value(false) -> "false".