%%%------------------------------------------------------------------- %%% @author anlicheng %%% @copyright (C) 2025, %%% @doc %%% %%% @end %%% Created : 07. 5月 2025 15:47 %%%------------------------------------------------------------------- -module(docker_deployer). -author("anlicheng"). -include("message_pb.hrl"). -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 :: message_pb:'ContainerDeployParams'()) -> {ok, {pid(), reference()}}). start_monitor(TaskId, ContainerDir, Params) when is_integer(TaskId), is_list(ContainerDir), is_record(Params, 'ContainerDeployParams') -> {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 :: message_pb:'ContainerDeployParams'()) -> ok. deploy(TaskId, ContainerDir, Params) when is_integer(TaskId), is_list(ContainerDir), is_record(Params, 'ContainerDeployParams') -> 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] -> <>; [_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 :: message_pb:'ContainerDeployParams'()) -> {ok, binary()}. create_container_and_config(TaskId, ContainerDir, Params) when is_integer(TaskId), is_list(ContainerDir), is_record(Params, 'ContainerDeployParams') -> 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(message_pb:'ContainerDeployParams'()) -> binary(). deploy_container_name(#'ContainerDeployParams'{container_name = ContainerName}) when ContainerName =/= undefined -> to_binary(ContainerName); deploy_container_name(_) -> throw({deploy_error, <<"invalid deploy params: container_name missing">>}). -spec deploy_image(message_pb:'ContainerDeployParams'()) -> binary(). deploy_image(#'ContainerDeployParams'{create = #'DockerCreateOptions'{config = #'DockerContainerConfig'{image = Image}}}) when Image =/= undefined -> to_binary(Image); deploy_image(_) -> throw({deploy_error, <<"invalid deploy params: image missing">>}). -spec to_binary(unicode:chardata()) -> binary(). to_binary(Value) when is_binary(Value) -> Value; to_binary(Value) when is_list(Value) -> unicode:characters_to_binary(Value).