ekfa/apps/efka/src/efka_inetd_task.erl
2025-09-16 15:42:11 +08:00

231 lines
8.8 KiB
Erlang
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 07. 5月 2025 15:47
%%%-------------------------------------------------------------------
-module(efka_inetd_task).
-author("anlicheng").
-include("efka_tables.hrl").
-behaviour(gen_server).
%% API
-export([start_link/3]).
-export([deploy/1]).
-export([maybe_create_env_file/2]).
-export([test/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-record(state, {
root_dir :: string(),
task_id :: integer(),
config = #{}
}).
test() ->
M = #{
<<"image">> => <<"docker.1ms.run/library/nginx:latest">>,
<<"container_name">> => <<"my_nginx">>,
<<"command">> => [<<"nginx">>,<<"-g">>,<<"daemon off;">>],
<<"entrypoint">> => [<<"/docker-entrypoint.sh">>],
<<"envs">> => [<<"ENV1=val1">>, <<"ENV2=val2">>],
<<"env_file">> => [<<"./env.list">>],
<<"ports">> => [<<"8080:80">>, <<"443:443">>],
<<"expose">> => [<<"80">>, <<"443">>],
<<"volumes">> => [<<"/host/data:/data">>, <<"/host/log:/var/log">>],
<<"networks">> => [<<"mynet">>],
<<"labels">> => #{ <<"role">> => <<"web">>, <<"env">> => <<"prod">> },
<<"restart">> => <<"always">>,
<<"user">> => <<"www-data">>,
<<"working_dir">> => <<"/app">>,
<<"hostname">> => <<"myhost">>,
<<"privileged">> => true,
<<"cap_add">> => [<<"NET_ADMIN">>],
<<"cap_drop">> => [<<"MKNOD">>],
<<"devices">> => [<<"/dev/snd:/dev/snd">>],
<<"mem_limit">> => <<"512m">>,
<<"mem_reservation">> => <<"256m">>,
<<"cpu_shares">> => 512,
<<"cpus">> => 1.5,
<<"ulimits">> => #{ <<"nofile">> => <<"1024:2048">> },
<<"sysctls">> => #{ <<"net.ipv4.ip_forward">> => <<"1">> },
<<"tmpfs">> => [<<"/tmp">>],
<<"extra_hosts">> => [<<"host1:192.168.0.1">>],
<<"healthcheck">> => #{
%<<"test">> => [<<"CMD-SHELL">>, <<"curl -f http://localhost || exit 1">>],
<<"test">> => [<<"CMD">>, <<"ls">>, <<"-l">>],
<<"interval">> => <<"30s">>,
<<"timeout">> => <<"10s">>,
<<"retries">> => 3
}
},
TaskId = 1,
RootDir = "/tmp/",
Res = do_deploy(TaskId, RootDir, M),
lager:debug("res is: ~p", [Res]).
%%%===================================================================
%%% API
%%%===================================================================
-spec deploy(Pid :: pid()) -> no_return().
deploy(Pid) when is_pid(Pid) ->
gen_server:cast(Pid, deploy).
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link(TaskId :: integer(), RootDir :: string(), Config :: map()) ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link(TaskId, RootDir, Config) when is_integer(TaskId), is_list(RootDir), is_map(Config) ->
gen_server:start_link(?MODULE, [TaskId, RootDir, Config], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([TaskId, RootDir, Config]) ->
{ok, #state{task_id = TaskId, root_dir = RootDir, config = Config}}.
%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_call(_Request, _From, State = #state{}) ->
{reply, ok, State}.
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_cast(deploy, State = #state{task_id = TaskId, root_dir = RootDir, config = Config}) ->
do_deploy(TaskId, RootDir, Config),
{stop, normal, State};
handle_cast(_Request, State) ->
{stop, normal, State}.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info(_Info, State = #state{}) ->
{noreply, State}.
%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) ->
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% 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 do_deploy(TaskId :: integer(), RootDir :: string(), Config :: map()) -> ok | {error, Reason :: any()}.
do_deploy(TaskId, RootDir, Config) when is_integer(TaskId), is_list(RootDir), is_map(Config) ->
%% 尝试拉取镜像
ContainerName = maps:get(<<"container_name">>, Config),
Image0 = maps:get(<<"image">>, Config),
Image = normalize_image(Image0),
{ok, ContainerDir} = ensure_dirs(RootDir, ContainerName),
case try_pull_image(Image) of
ok ->
%% 创建container
%% 如果存在envs参数则生成环境变量参数文件
%maybe_create_env_file(ContainerDir, maps:get(<<"envs">>, Config, [])),
%% 创建镜像, 并预留配置文件的绑定位置: "/etc/容器名称/"
case efka_docker_command:check_container_exist(ContainerName) of
true ->
{error, <<"container exist">>};
false ->
case efka_docker_command:create_container(ContainerName, ContainerDir, Config) of
{ok, ContainerId} ->
{ok, ContainerId};
{error, Reason} ->
{error, Reason}
end
end;
{error, Reason} ->
{error, Reason}
end.
-spec try_pull_image(Image :: binary()) -> ok | {error, Reason :: any()}.
try_pull_image(Image) when is_binary(Image) ->
case efka_docker_command:check_image_exist(Image) of
true ->
ok;
false ->
efka_docker_command:pull_image(Image)
end.
maybe_create_env_file(_ContainerDir, []) ->
ok;
maybe_create_env_file(ContainerDir, Envs) when is_list(Envs)->
lager:debug("envs: ~p", [Envs]),
TargetFile = ContainerDir ++ "env",
{ok, IoDevice} = file:open(TargetFile, [write, binary]),
lists:foreach(fun(Env) -> file:write(IoDevice, <<Env/binary, $\n>>) end, Envs),
ok = file:close(IoDevice),
{ok, TargetFile}.
-spec ensure_dirs(RootDir :: string(), ContainerName :: binary()) -> {ok, ServerRootDir :: string()}.
ensure_dirs(RootDir, ContainerName) when is_list(RootDir), is_binary(ContainerName) ->
%% 根目录
ServiceRootDir = RootDir ++ "/" ++ binary_to_list(ContainerName) ++ "/",
ok = filelib:ensure_dir(ServiceRootDir),
{ok, ServiceRootDir}.
-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])).