212 lines
9.5 KiB
Erlang
212 lines
9.5 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @author anlicheng
|
|
%%% @copyright (C) 2025, <COMPANY>
|
|
%%% @doc
|
|
%%%
|
|
%%% @end
|
|
%%% Created : 07. 5月 2025 15:47
|
|
%%%-------------------------------------------------------------------
|
|
-module(docker_deployer).
|
|
-author("anlicheng").
|
|
-dialyzer([{nowarn_function, normalize_image/1}]).
|
|
|
|
%% API
|
|
-export([start_monitor/3]).
|
|
-export([deploy/3]).
|
|
|
|
-define(TASK_SUCCESS, <<"success">>).
|
|
-define(TASK_FAIL, <<"fail">>).
|
|
|
|
%%%===================================================================
|
|
%%% API
|
|
%%%===================================================================
|
|
|
|
-spec(start_monitor(TaskId :: integer(), ContainerDir :: string(), Params :: map()) ->
|
|
{ok, {pid(), reference()}}).
|
|
start_monitor(TaskId, ContainerDir, Params)
|
|
when is_integer(TaskId), is_list(ContainerDir), is_map(Params) ->
|
|
{ok, spawn_monitor(?MODULE, deploy, [TaskId, ContainerDir, Params])}.
|
|
|
|
%%%===================================================================
|
|
%%% Internal functions
|
|
%%%===================================================================
|
|
|
|
%{
|
|
% "image": "nginx:latest",
|
|
% "container_name": "my_nginx",
|
|
% "ports": ["8080:80", "443:443"],
|
|
% "volumes": ["/host/data:/data", "/host/log:/var/log"],
|
|
% "envs": ["ENV1=val1", "ENV2=val2"],
|
|
% "entrypoint": ["/docker-entrypoint.sh"],
|
|
% "command": ["nginx", "-g", "daemon off;"],
|
|
% "restart": "always"
|
|
%}
|
|
-spec deploy(TaskId :: integer(), ContainerDir :: string(), Params :: map()) -> ok.
|
|
deploy(TaskId, ContainerDir, Params) when is_integer(TaskId), is_list(ContainerDir), is_map(Params) ->
|
|
ContainerName = deploy_container_name(Params),
|
|
Image0 = deploy_image(Params),
|
|
report_task_event(TaskId, <<"info">>, <<"开始部署容器:"/utf8, ContainerName/binary>>),
|
|
try
|
|
ok = ensure_container_absent(TaskId, ContainerName),
|
|
{ok, Image} = ensure_image_ready(TaskId, Image0),
|
|
{ok, ContainerId} = create_container_and_config(TaskId, ContainerDir, Params),
|
|
ShortContainerId = short_container_id(ContainerId),
|
|
report_task_event(TaskId, <<"info">>, <<"容器创建成功: "/utf8, ShortContainerId/binary>>),
|
|
report_task_event(TaskId, <<"info">>, <<"任务完成"/utf8>>),
|
|
write_task_summary(TaskId, <<"success">>, ContainerName, Image, ContainerId),
|
|
docker_task_reporter:close(TaskId, ?TASK_SUCCESS)
|
|
catch
|
|
throw:{deploy_error, Reason} ->
|
|
report_task_event(TaskId, <<"error">>, Reason),
|
|
report_task_event(TaskId, <<"error">>, <<"任务失败"/utf8>>),
|
|
write_task_summary(TaskId, <<"fail">>, ContainerName, Image0, undefined),
|
|
write_task_failure_reason(TaskId, Reason),
|
|
docker_task_reporter:close(TaskId, ?TASK_FAIL);
|
|
Class:Reason:Stacktrace ->
|
|
Error = iolist_to_binary(io_lib:format("deploy crashed: ~p:~p ~p", [Class, Reason, Stacktrace])),
|
|
report_task_event(TaskId, <<"error">>, Error),
|
|
report_task_event(TaskId, <<"error">>, <<"任务失败"/utf8>>),
|
|
write_task_summary(TaskId, <<"fail">>, ContainerName, Image0, undefined),
|
|
write_task_failure_reason(TaskId, Error),
|
|
docker_task_reporter:close(TaskId, ?TASK_FAIL)
|
|
end.
|
|
|
|
-spec normalize_image(binary()) -> binary().
|
|
normalize_image(Image) when is_binary(Image) ->
|
|
Parts = binary:split(Image, <<"/">>, [global]),
|
|
{PrefixParts, [Last]} = lists:split(length(Parts) - 1, Parts),
|
|
NormalizedLast = case binary:split(Last, <<":">>, [global]) of
|
|
[_Name] -> <<Last/binary, ":latest">>;
|
|
[_Name, _Tag] -> Last
|
|
end,
|
|
iolist_to_binary(lists:join(<<"/">>, PrefixParts ++ [NormalizedLast])).
|
|
|
|
-spec report_task_event(TaskId :: integer(), Level :: binary(), Msg :: binary()) -> ok.
|
|
report_task_event(TaskId, Level, Msg) when is_integer(TaskId), is_binary(Level), is_binary(Msg) ->
|
|
docker_task_reporter:stream(TaskId, Level, Msg).
|
|
|
|
-spec write_task_summary(integer(), binary(), binary(), binary(), undefined | binary()) -> ok.
|
|
write_task_summary(TaskId, Status, ContainerName, Image, ContainerId)
|
|
when is_integer(TaskId), is_binary(Status), is_binary(ContainerName), is_binary(Image) ->
|
|
Fields0 = [
|
|
<<"type=deploy_summary">>,
|
|
<<"task_id=">>, integer_to_binary(TaskId),
|
|
<<" status=">>, Status,
|
|
<<" container_name=">>, ContainerName,
|
|
<<" image=">>, Image
|
|
],
|
|
Fields = case ContainerId of
|
|
undefined ->
|
|
Fields0;
|
|
ContainerId0 when is_binary(ContainerId0) ->
|
|
Fields0 ++ [<<" container_id=">>, short_container_id(ContainerId0)]
|
|
end,
|
|
efka_logger:write(iolist_to_binary(Fields)).
|
|
|
|
-spec write_task_failure_reason(integer(), binary()) -> ok.
|
|
write_task_failure_reason(TaskId, Reason) when is_integer(TaskId), is_binary(Reason) ->
|
|
Info = iolist_to_binary([
|
|
<<"type=deploy_failure_reason task_id=">>,
|
|
integer_to_binary(TaskId),
|
|
<<" reason=">>,
|
|
Reason
|
|
]),
|
|
efka_logger:write(Info).
|
|
|
|
-spec ensure_container_absent(TaskId :: integer(), ContainerName :: binary()) -> ok.
|
|
ensure_container_absent(TaskId, ContainerName) when is_integer(TaskId), is_binary(ContainerName) ->
|
|
report_task_event(TaskId, <<"info">>, <<"开始创建容器: "/utf8, ContainerName/binary>>),
|
|
ok.
|
|
|
|
-spec ensure_image_ready(TaskId :: integer(), Image0 :: binary()) -> {ok, binary()}.
|
|
ensure_image_ready(TaskId, Image0) when is_integer(TaskId), is_binary(Image0) ->
|
|
Image = normalize_image(Image0),
|
|
report_task_event(TaskId, <<"info">>, <<"使用镜像:"/utf8, Image/binary>>),
|
|
report_task_event(TaskId, <<"info">>, <<"开始拉取镜像:"/utf8, Image/binary>>),
|
|
case docker_commands:pull_image(Image) of
|
|
{ok, Ref, Pid, MRef} ->
|
|
await_pull_image(TaskId, Ref, Pid, MRef),
|
|
{ok, Image}
|
|
end.
|
|
|
|
-spec await_pull_image(TaskId :: integer(), Ref :: reference(), Pid :: pid(), MRef :: reference()) -> ok.
|
|
await_pull_image(TaskId, Ref, Pid, MRef) ->
|
|
receive
|
|
{docker_client, Ref, {response, _Status, _Headers}} ->
|
|
await_pull_image(TaskId, Ref, Pid, MRef);
|
|
{docker_client, Ref, {data, Data}} ->
|
|
report_task_event(TaskId, <<"info">>, Data),
|
|
await_pull_image(TaskId, Ref, Pid, MRef);
|
|
{docker_client, Ref, done} ->
|
|
erlang:demonitor(MRef, [flush]),
|
|
ok;
|
|
{docker_client, Ref, {error, Reason}} ->
|
|
erlang:demonitor(MRef, [flush]),
|
|
report_task_event(TaskId, <<"error">>, Reason),
|
|
throw({deploy_error, <<"镜像拉取失败: "/utf8, Reason/binary>>});
|
|
{'DOWN', MRef, process, Pid, Reason0} ->
|
|
Reason = iolist_to_binary(io_lib:format("~p", [Reason0])),
|
|
throw({deploy_error, <<"镜像拉取失败: "/utf8, Reason/binary>>})
|
|
end.
|
|
|
|
-spec create_container_and_config(TaskId :: integer(), ContainerDir :: string(), Params :: map()) ->
|
|
{ok, binary()}.
|
|
create_container_and_config(TaskId, ContainerDir, Params)
|
|
when is_integer(TaskId), is_list(ContainerDir), is_map(Params) ->
|
|
case docker_commands:create_container(ContainerDir, Params) of
|
|
{ok, ContainerId} ->
|
|
ok = create_config_file(TaskId, ContainerDir),
|
|
{ok, ContainerId};
|
|
{error, Reason} when is_binary(Reason) ->
|
|
throw({deploy_error, format_create_container_error(Reason)});
|
|
{error, Reason} ->
|
|
Error = iolist_to_binary(io_lib:format("容器创建失败: ~p", [Reason])),
|
|
throw({deploy_error, Error})
|
|
end.
|
|
|
|
-spec create_config_file(TaskId :: integer(), ContainerDir :: string()) -> ok.
|
|
create_config_file(TaskId, ContainerDir) when is_integer(TaskId), is_list(ContainerDir) ->
|
|
ConfigFile = docker_helper:get_config_file(ContainerDir),
|
|
case file:open(ConfigFile, [write, exclusive]) of
|
|
{ok, FD} ->
|
|
ok = file:write(FD, <<>>),
|
|
file:close(FD),
|
|
ok;
|
|
{error, Reason} ->
|
|
ReasonBin = list_to_binary(io_lib:format("~p", [Reason])),
|
|
report_task_event(TaskId, <<"notice">>, <<"创建配置文件失败: "/utf8, ReasonBin/binary>>),
|
|
ok
|
|
end.
|
|
|
|
-spec format_create_container_error(binary()) -> binary().
|
|
format_create_container_error(Reason) when is_binary(Reason) ->
|
|
case is_container_already_exists_error(Reason) of
|
|
true ->
|
|
<<"本地容器已经存在"/utf8>>;
|
|
false ->
|
|
<<"容器创建失败: "/utf8, Reason/binary>>
|
|
end.
|
|
|
|
-spec is_container_already_exists_error(binary()) -> boolean().
|
|
is_container_already_exists_error(Reason) when is_binary(Reason) ->
|
|
binary:match(Reason, <<"is already in use by container">>) =/= nomatch orelse
|
|
binary:match(Reason, <<"Conflict. The container name ">>) =/= nomatch.
|
|
|
|
-spec short_container_id(binary()) -> binary().
|
|
short_container_id(ContainerId) when is_binary(ContainerId), byte_size(ContainerId) >= 12 ->
|
|
binary:part(ContainerId, 0, 12);
|
|
short_container_id(ContainerId) when is_binary(ContainerId) ->
|
|
ContainerId.
|
|
|
|
-spec deploy_container_name(map()) -> binary().
|
|
deploy_container_name(#{<<"container_name">> := ContainerName}) when is_binary(ContainerName) ->
|
|
ContainerName;
|
|
deploy_container_name(_) ->
|
|
throw({deploy_error, <<"invalid deploy params: container_name missing">>}).
|
|
|
|
-spec deploy_image(map()) -> binary().
|
|
deploy_image(#{<<"create">> := #{<<"config">> := #{<<"image">> := Image}}}) when is_binary(Image) ->
|
|
Image;
|
|
deploy_image(_) ->
|
|
throw({deploy_error, <<"invalid deploy params: image missing">>}).
|