296 lines
12 KiB
Erlang
296 lines
12 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @author anlicheng
|
|
%%% @copyright (C) 2025, <COMPANY>
|
|
%%% @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".
|