ekfa/src/docker/docker_client.erl
2026-04-23 17:38:17 +08:00

279 lines
11 KiB
Erlang

%%%-------------------------------------------------------------------
%%% @doc
%%% Short-lived Docker API client process.
%%%
%%% Each request owns one process and one gun Unix socket connection.
%%% The process exits after the Docker API response finishes.
%%% @end
%%%-------------------------------------------------------------------
-module(docker_client).
%% API
-export([request/4, start_stream/5]).
-define(DOCKER_SOCKET, "/var/run/docker.sock").
-define(RESPONSE_TIMEOUT, 5000).
-define(BODY_TIMEOUT, 10000).
-define(STREAM_BODY_TIMEOUT, 30000).
-define(CLIENT_TIMEOUT, 60000).
%%%===================================================================
%%% API
%%%===================================================================
-spec request(Method :: string(), Path :: string(), Body :: binary(), Headers :: list()) ->
{ok, StatusCode :: integer(), RespHeaders :: proplists:proplist(), RespBody :: binary()} | {error, any()}.
request(Method, Path, Body, Headers) when is_list(Method), is_list(Path), is_binary(Body), is_list(Headers) ->
Owner = self(),
Ref = make_ref(),
{Pid, MRef} = spawn_monitor(fun() -> request_worker(Owner, Ref, Method, Path, Body, Headers) end),
receive
{docker_client, Ref, {result, Result}} ->
erlang:demonitor(MRef, [flush]),
Result;
{'DOWN', MRef, process, Pid, Reason} ->
{error, {client_down, Reason}}
after ?CLIENT_TIMEOUT ->
exit(Pid, shutdown),
erlang:demonitor(MRef, [flush]),
{error, timeout}
end.
-spec start_stream(Owner :: pid(), Method :: string(), Path :: string(), Body :: binary(), Headers :: list()) ->
{ok, Ref :: reference(), Pid :: pid(), MRef :: reference()}.
start_stream(Owner, Method, Path, Body, Headers)
when is_pid(Owner), is_list(Method), is_list(Path), is_binary(Body), is_list(Headers) ->
Ref = make_ref(),
{Pid, MRef} = spawn_monitor(fun() -> stream_worker(Owner, Ref, Method, Path, Body, Headers) end),
{ok, Ref, Pid, MRef}.
%%%===================================================================
%%% Worker functions
%%%===================================================================
-spec request_worker(pid(), reference(), string(), string(), binary(), list()) -> ok.
request_worker(Owner, Ref, Method, Path, Body, Headers) ->
Result = do_request(Method, Path, Body, Headers),
send_owner(Owner, Ref, {result, Result}).
-spec stream_worker(pid(), reference(), string(), string(), binary(), list()) -> ok.
stream_worker(Owner, Ref, Method, Path, Body, Headers) ->
OwnerRef = erlang:monitor(process, Owner),
_Result = do_stream(Owner, OwnerRef, Ref, Method, Path, Body, Headers),
erlang:demonitor(OwnerRef, [flush]),
ok.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec do_request(string(), string(), binary(), list()) ->
{ok, integer(), proplists:proplist(), binary()} | {error, any()}.
do_request(Method, Path, Body, Headers) ->
case open_connection() of
{ok, ConnPid} ->
try
StreamRef = gun:request(ConnPid, Method, Path, Headers, Body),
receive_response(ConnPid, StreamRef)
after
close_connection(ConnPid)
end;
{error, Reason} ->
{error, Reason}
end.
-spec do_stream(pid(), reference(), reference(), string(), string(), binary(), list()) ->
ok | {error, binary()}.
do_stream(Owner, OwnerRef, Ref, Method, Path, Body, Headers) ->
case open_connection() of
{ok, ConnPid} ->
try
StreamRef = gun:request(ConnPid, Method, Path, Headers, Body),
receive_stream_response(Owner, OwnerRef, Ref, ConnPid, StreamRef)
after
close_connection(ConnPid)
end;
{error, Reason0} ->
Reason = format_error(Reason0),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason}
end.
-spec receive_response(pid(), reference()) ->
{ok, integer(), proplists:proplist(), binary()} | {error, any()}.
receive_response(ConnPid, StreamRef) ->
receive
{gun_response, ConnPid, StreamRef, nofin, Status, Headers} ->
receive_body(ConnPid, StreamRef, Status, Headers, <<>>);
{gun_response, ConnPid, StreamRef, fin, Status, Headers} ->
{ok, Status, Headers, <<>>};
{gun_error, ConnPid, StreamRef, Reason} ->
{error, {http_error, Reason}};
{gun_error, ConnPid, Reason} ->
{error, {http_error, Reason}};
{gun_down, ConnPid, _, Reason, _} ->
{error, {http_closed, Reason}}
after ?RESPONSE_TIMEOUT ->
{error, timeout}
end.
-spec receive_body(pid(), reference(), integer(), proplists:proplist(), iodata()) ->
{ok, integer(), proplists:proplist(), binary()} | {error, any()}.
receive_body(ConnPid, StreamRef, Status, Headers, Acc) ->
receive
{gun_data, ConnPid, StreamRef, fin, Data} ->
Body = iolist_to_binary([Acc, Data]),
{ok, Status, Headers, Body};
{gun_data, ConnPid, StreamRef, nofin, Data} ->
receive_body(ConnPid, StreamRef, Status, Headers, [Acc, Data]);
{gun_error, ConnPid, StreamRef, Reason} ->
{error, {http_error, Reason}};
{gun_error, ConnPid, Reason} ->
{error, {http_error, Reason}};
{gun_down, ConnPid, _, Reason, _} ->
{error, {http_closed, Reason}}
after ?BODY_TIMEOUT ->
{error, timeout}
end.
-spec receive_stream_response(pid(), reference(), reference(), pid(), reference()) -> ok | {error, binary()}.
receive_stream_response(Owner, OwnerRef, Ref, ConnPid, StreamRef) ->
receive
{gun_response, ConnPid, StreamRef, nofin, Status, Headers} when Status >= 200, Status < 300 ->
send_owner(Owner, Ref, {response, Status, Headers}),
receive_stream_body(Owner, OwnerRef, Ref, ConnPid, StreamRef);
{gun_response, ConnPid, StreamRef, fin, Status, Headers} when Status >= 200, Status < 300 ->
send_owner(Owner, Ref, {response, Status, Headers}),
send_owner(Owner, Ref, done),
ok;
{gun_response, ConnPid, StreamRef, nofin, Status, _Headers} ->
receive_stream_error_body(Owner, OwnerRef, Ref, ConnPid, StreamRef, Status, <<>>);
{gun_response, ConnPid, StreamRef, fin, Status, _Headers} ->
Reason = http_status_error(Status, <<>>),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason};
{gun_error, ConnPid, StreamRef, Reason0} ->
Reason = format_error(Reason0),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason};
{gun_error, ConnPid, Reason0} ->
Reason = format_error(Reason0),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason};
{gun_down, ConnPid, _, Reason0, _} ->
Reason = format_error(Reason0),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason};
{'DOWN', OwnerRef, process, _Pid, Reason0} ->
{error, format_error({owner_down, Reason0})}
after ?RESPONSE_TIMEOUT ->
send_owner(Owner, Ref, {error, <<"处理超时"/utf8>>}),
{error, <<"timeout">>}
end.
-spec receive_stream_body(pid(), reference(), reference(), pid(), reference()) -> ok | {error, binary()}.
receive_stream_body(Owner, OwnerRef, Ref, ConnPid, StreamRef) ->
receive
{gun_data, ConnPid, StreamRef, fin, Data} ->
maybe_send_stream_data(Owner, Ref, Data),
send_owner(Owner, Ref, done),
ok;
{gun_data, ConnPid, StreamRef, nofin, Data} ->
maybe_send_stream_data(Owner, Ref, Data),
receive_stream_body(Owner, OwnerRef, Ref, ConnPid, StreamRef);
{gun_error, ConnPid, StreamRef, Reason0} ->
Reason = format_error(Reason0),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason};
{gun_error, ConnPid, Reason0} ->
Reason = format_error(Reason0),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason};
{gun_down, ConnPid, _, Reason0, _} ->
Reason = format_error(Reason0),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason};
{'DOWN', OwnerRef, process, _Pid, Reason0} ->
{error, format_error({owner_down, Reason0})}
after ?STREAM_BODY_TIMEOUT ->
send_owner(Owner, Ref, {error, <<"timeout">>}),
{error, <<"timeout">>}
end.
-spec receive_stream_error_body(pid(), reference(), reference(), pid(), reference(), integer(), iodata()) ->
{error, binary()}.
receive_stream_error_body(Owner, OwnerRef, Ref, ConnPid, StreamRef, Status, Acc) ->
receive
{gun_data, ConnPid, StreamRef, fin, Data} ->
Reason = http_status_error(Status, iolist_to_binary([Acc, Data])),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason};
{gun_data, ConnPid, StreamRef, nofin, Data} ->
receive_stream_error_body(Owner, OwnerRef, Ref, ConnPid, StreamRef, Status, [Acc, Data]);
{gun_error, ConnPid, StreamRef, Reason0} ->
Reason = format_error(Reason0),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason};
{gun_error, ConnPid, Reason0} ->
Reason = format_error(Reason0),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason};
{gun_down, ConnPid, _, Reason0, _} ->
Reason = format_error(Reason0),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason};
{'DOWN', OwnerRef, process, _Pid, Reason0} ->
{error, format_error({owner_down, Reason0})}
after ?BODY_TIMEOUT ->
Reason = http_status_error(Status, iolist_to_binary(Acc)),
send_owner(Owner, Ref, {error, Reason}),
{error, Reason}
end.
-spec open_connection() -> {ok, pid()} | {error, any()}.
open_connection() ->
case gun:open_unix(?DOCKER_SOCKET, #{}) of
{ok, ConnPid} ->
case gun:await_up(ConnPid) of
{ok, _} ->
{ok, ConnPid};
{error, Reason} ->
close_connection(ConnPid),
{error, Reason}
end;
{error, Reason} ->
{error, Reason}
end.
-spec close_connection(pid()) -> ok.
close_connection(ConnPid) ->
catch gun:close(ConnPid),
ok.
-spec send_owner(pid(), reference(), term()) -> ok.
send_owner(Owner, Ref, Event) ->
Owner ! {docker_client, Ref, Event},
ok.
-spec maybe_send_stream_data(pid(), reference(), iodata()) -> ok.
maybe_send_stream_data(Owner, Ref, Data) ->
case iolist_to_binary(Data) of
<<>> ->
ok;
DataBin ->
send_owner(Owner, Ref, {data, DataBin})
end.
-spec http_status_error(integer(), binary()) -> binary().
http_status_error(Status, <<>>) ->
iolist_to_binary(io_lib:format("docker http status ~B", [Status]));
http_status_error(Status, Body) when is_binary(Body) ->
StatusBin = integer_to_binary(Status),
<<"docker http status ", StatusBin/binary, ": ", Body/binary>>.
-spec format_error(term()) -> binary().
format_error(Reason) when is_binary(Reason) ->
Reason;
format_error(Reason) ->
iolist_to_binary(io_lib:format("~p", [Reason])).