Current section
Files
Jump to
Current section
Files
src/kill_bill.erl
%%
%% Copyright 2013-16 Joaquim Rocha
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
-module(kill_bill).
-behaviour(gen_server).
-define(PROTOCOL_HTTP, http).
-define(PROTOCOL_HTTPS, https).
-define(SERVER, {local, ?MODULE}).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([start_link/0]).
-export([config_server/1, start_server/1, stop_server/1, get_server_list/0]).
-export([deploy/2, undeploy/1, get_webapp_list/0]).
%% ====================================================================
%% API functions
%% ====================================================================
start_link() ->
gen_server:start_link(?SERVER, ?MODULE, [], []).
-spec config_server(ServerConfig :: Config) -> {ok, ServerName :: atom()} | {error, Reason :: any()}
when Config :: ConfigFile :: string()
| {server_config, ServerName :: atom(), Config :: list()}.
config_server({server_config, ServerName, Config}) when is_atom(ServerName) andalso is_list(Config) ->
gen_server:call(?MODULE, {config_server, ServerName, Config});
config_server(FileName) when is_list(FileName) ->
case load_configuration(FileName) of
not_found -> {error, not_found};
{error, Reason} -> {error, Reason};
{ok, Config} -> config_server(Config)
end.
-spec start_server(ServerName :: atom()) -> ok | {error, Reason :: any()}.
start_server(ServerName) when is_atom(ServerName) ->
gen_server:call(?MODULE, {start_server, ServerName}).
-spec stop_server(ServerName :: atom()) -> ok | {error, Reason :: any()}.
stop_server(ServerName) when is_atom(ServerName) ->
gen_server:call(?MODULE, {stop_server, ServerName}).
-spec get_server_list() -> list().
get_server_list() ->
gen_server:call(?MODULE, {get_server_list}).
-spec deploy(ServerName :: atom(), WebAppConfig :: Config) -> ok | {error, Reason :: any()}
when Config :: WebAppFile :: string()
| {webapp_config, WebAppName :: atom(), Config :: list()}.
deploy(ServerName, {webapp_config, WebAppName, Config}) when is_atom(ServerName) andalso is_atom(WebAppName) andalso is_list(Config) ->
gen_server:call(?MODULE, {deploy, ServerName, {WebAppName, Config}});
deploy(ServerName, FileName) when is_atom(ServerName) andalso is_list(FileName) ->
case load_configuration(FileName) of
not_found -> {error, not_found};
{error, Reason} -> {error, Reason};
{ok, Config} -> deploy(ServerName, Config)
end.
-spec undeploy(WebAppName :: atom()) -> ok | {error, Reason :: any()}.
undeploy(WebAppName) when is_atom(WebAppName) ->
gen_server:call(?MODULE, {undeploy, WebAppName}).
-spec get_webapp_list() -> list().
get_webapp_list() ->
gen_server:call(?MODULE, {get_webapp_list}).
%% ====================================================================
%% Behavioural functions
%% ====================================================================
-record(server, {config, webapps = [], running=false}).
-record(webapp, {config, resource, server, session}).
-record(status, {servers, webapps}).
init([]) ->
process_flag(trap_exit, true),
error_logger:info_msg("Bang Bang, ~p [~p] Starting...\n", [?MODULE, self()]),
{ok, #status{servers=dict:new(), webapps=dict:new()}}.
handle_call({config_server, ServerName, Config}, _From, State=#status{servers=Servers}) ->
Reply = case dict:find(ServerName, Servers) of
error ->
NServers = dict:store(ServerName, #server{config=Config}, Servers),
NState = State#status{servers=NServers},
error_logger:info_msg("Server ~p configured: ~p\n", [ServerName, Config]),
{ok, ServerName};
{ok, #server{running=false, webapps=WebApps}} ->
NServers = dict:store(ServerName, #server{config=Config, webapps=WebApps}, Servers),
NState = State#status{servers=NServers},
error_logger:info_msg("Server ~p reconfigured: ~p\n", [ServerName, Config]),
{ok, ServerName};
{ok, _Server} ->
error_logger:error_msg("KB: Server ~p is running\n", [ServerName]),
NState = State,
{error, server_running}
end,
{reply, Reply, NState};
handle_call({start_server, ServerName}, _From, State=#status{servers=Servers, webapps=Webapps}) ->
Reply = case dict:find(ServerName, Servers) of
error ->
error_logger:error_msg("KB: Server ~p not found\n", [ServerName]),
NState = State,
{error, not_found};
{ok, #server{running=true}} ->
error_logger:info_msg("Server ~p was already running!\n", [ServerName]),
NState = State,
ok;
{ok, Server} ->
Protocol = proplists:get_value(protocol, Server#server.config, ?PROTOCOL_HTTP),
NbAcceptors = proplists:get_value(acceptor_number, Server#server.config, 100),
Host = get_host(Server#server.config),
Port = get_port(Server#server.config),
PathsList = get_server_paths(ServerName, Host, Server, Webapps),
Dispatch = cowboy_router:compile([{Host, PathsList}]),
ProtoOpts = [{env, [{dispatch, Dispatch}]}],
TransOpts = get_server_config(Protocol, Port, Server#server.config),
case Protocol of
?PROTOCOL_HTTP ->
{ok, _} = cowboy:start_http(ServerName, NbAcceptors, TransOpts, ProtoOpts);
?PROTOCOL_HTTPS ->
{ok, _} = cowboy:start_https(ServerName, NbAcceptors, TransOpts, ProtoOpts)
end,
error_logger:info_msg("Server ~p started!\n", [ServerName]),
NServers = dict:store(ServerName, Server#server{running=true}, Servers),
NState = State#status{servers=NServers},
ok
end,
{reply, Reply, NState};
handle_call({stop_server, ServerName}, _From, State=#status{servers=Servers}) ->
Reply = case dict:find(ServerName, Servers) of
error ->
error_logger:error_msg("KB: Server ~p not found\n", [ServerName]),
NState = State,
{error, not_found};
{ok, #server{running=false}} ->
error_logger:info_msg("Server ~p was not running!\n", [ServerName]),
NState = State,
ok;
{ok, Server} ->
cowboy:stop_listener(ServerName),
error_logger:info_msg("Server ~p stoped!\n", [ServerName]),
NServers = dict:store(ServerName, Server#server{running=false}, Servers),
NState = State#status{servers=NServers},
ok
end,
{reply, Reply, NState};
handle_call({get_server_list}, From, State=#status{servers=Servers}) ->
Fun = fun() ->
Rep = fun(ServerName, #server{running=Run, webapps=WebApps}, Acc) ->
[{ServerName, Run, WebApps} | Acc]
end,
Reply = dict:fold(Rep, [], Servers),
gen_server:reply(From, Reply)
end,
spawn(Fun),
{noreply, State};
handle_call({deploy, ServerName, {WebAppName, Config}}, _From, State=#status{servers=Servers, webapps=Webapps}) ->
Reply = case dict:find(WebAppName, Webapps) of
error ->
case dict:find(ServerName, Servers) of
error ->
error_logger:error_msg("KB: Server ~p not configured\n", [ServerName]),
NState = State,
{error, not_found};
{ok, Server} ->
Resource = create_resource_server(Config),
SessionManager = create_session(WebAppName, Config),
WebApp = #webapp{config=Config, resource=Resource, server=ServerName, session=SessionManager},
NWebapps = dict:store(WebAppName, WebApp, Webapps),
NServer = Server#server{webapps=[WebAppName | Server#server.webapps]},
NServers = dict:store(ServerName, NServer, Servers),
case NServer#server.running of
true ->
Host = get_host(NServer#server.config),
PathsList = get_server_paths(ServerName, Host, NServer, NWebapps),
Dispatch = cowboy_router:compile([{Host, PathsList}]),
cowboy:set_env(ServerName, dispatch, Dispatch);
false -> ok
end,
error_logger:info_msg("WebApp ~p deployed on server ~p, with Config: ~p\n", [WebAppName, ServerName, Config]),
NState = State#status{servers=NServers, webapps=NWebapps},
ok
end;
{ok, _App} ->
error_logger:error_msg("KB: Duplicated webapp ~p\n", [WebAppName]),
NState = State,
{error, duplicated}
end,
{reply, Reply, NState};
handle_call({undeploy, WebAppName}, _From, State=#status{servers=Servers, webapps=Webapps}) ->
Reply = case dict:find(WebAppName, Webapps) of
error ->
error_logger:error_msg("KB: WebApp ~p not found\n", [WebAppName]),
NState = State,
{error, not_found};
{ok, WebApp} ->
NWebapps = dict:erase(WebAppName, Webapps),
{ok, Server} = dict:find(WebApp#webapp.server, Servers),
NServer = Server#server{webapps=lists:delete(WebAppName, Server#server.webapps)},
NServers = dict:store(WebApp#webapp.server, NServer, Servers),
case NServer#server.running of
true ->
Host = get_host(NServer#server.config),
PathsList = get_server_paths(WebApp#webapp.server, Host, NServer, NWebapps),
Dispatch = cowboy_router:compile([{Host, PathsList}]),
cowboy:set_env(WebApp#webapp.server, dispatch, Dispatch);
false -> ok
end,
stop_resource_server(WebApp#webapp.resource),
stop_session(WebApp#webapp.session),
error_logger:info_msg("WebApp ~p was undeployed from server ~p\n", [WebAppName, WebApp#webapp.server]),
NState = State#status{servers=NServers, webapps=NWebapps},
ok
end,
{reply, Reply, NState};
handle_call({get_webapp_list}, From, State=#status{webapps=Webapps}) ->
Fun = fun() ->
Rep = fun(WebAppName, #webapp{server=ServerName}, Acc) ->
[{WebAppName, ServerName} | Acc]
end,
Reply = dict:fold(Rep, [], Webapps),
gen_server:reply(From, Reply)
end,
spawn(Fun),
{noreply, State}.
handle_cast(Msg, State) ->
error_logger:info_msg("handle_cast(~p)\n", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
error_logger:info_msg("handle_info(~p)\n", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
error_logger:info_msg("Bang Bang, My Baby Shot Me Down\n"),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% ====================================================================
%% Internal functions
%% ====================================================================
load_configuration(FileName) ->
case filelib:is_file(FileName) of
true -> file:script(FileName);
false -> not_found
end.
get_host(ServerConfig) ->
proplists:get_value(host, ServerConfig, '_').
get_port(ServerConfig) ->
proplists:get_value(port, ServerConfig, 8080).
get_server_config(Protocol, Port, ServerConfig) ->
MaxCon = proplists:get_value(max_connections, ServerConfig, infinity),
SslConfig = proplists:get_value(ssl, ServerConfig),
Ssl = get_ssl(Protocol, SslConfig),
lists:append([{port, Port}, {max_connections, MaxCon}], Ssl).
get_ssl(?PROTOCOL_HTTP, _Ssl) -> [];
get_ssl(_Protocol, none) -> [];
get_ssl(_Protocol, Ssl) ->
[
{cacertfile, proplists:get_value(cacertfile, Ssl)},
{certfile, proplists:get_value(certfile, Ssl)},
{keyfile, proplists:get_value(keyfile, Ssl)}
].
create_resource_server(WebAppConfig) ->
ResourceConfig = proplists:get_value(resource, WebAppConfig, none),
get_resource_server(ResourceConfig).
get_resource_server(none) -> none;
get_resource_server(ResourceConfig) ->
{ok, Pid} = kb_resource_sup:start_resource_server(ResourceConfig),
Pid.
stop_resource_server(none) -> ok;
stop_resource_server(Pid) -> kb_resource:stop(Pid).
create_session(WebAppName, WebAppConfig) ->
case proplists:get_value(session_timeout, WebAppConfig, 30) of
none -> none;
SessionTimeout ->
SessionCache = list_to_atom(atom_to_list(WebAppName) ++ "_session"),
Options = [{max_age, SessionTimeout * 60},
{purge_interval, 60},
{cluster_nodes, all},
{sync_mode, full}],
'Elixir.Gibreel':create_cache(SessionCache, Options),
SessionCache
end.
stop_session(none) -> ok;
stop_session(SessionCache) ->
'Elixir.Gibreel':delete_cache(SessionCache).
get_server_paths(ServerName, Host, Server, Webapps) ->
case Server#server.webapps of
[] ->
[{'_', kb_dummy_toppage, [{server, ServerName}, {host, Host}]}];
WAList ->
AppList = get_app_list(WAList, Webapps, []),
get_web_app_config(AppList, [])
end.
get_app_list([], _Webapps, AppList) ->
Fun = fun({_, A, _}, {_, B, _}) -> A > B end,
lists:sort(Fun, AppList);
get_app_list([WebAppName|T], Webapps, AppList) ->
{ok, WebApp} = dict:find(WebAppName, Webapps),
Context = get_context(proplists:get_value(context, WebApp#webapp.config, "/")),
get_app_list(T, Webapps, [{WebAppName, Context, WebApp} | AppList]).
remove_slashs(Path) ->
kb_util:remove_if_ends_with(kb_util:remove_if_starts_with(Path, "/"), "/").
get_web_app_config([], Paths) -> lists:reverse(Paths);
get_web_app_config([{_WebAppName, Context, WebApp} | T], Paths) ->
ResourceServer = WebApp#webapp.resource,
SessionManager = WebApp#webapp.session,
TemplateConfig = proplists:get_value(template, WebApp#webapp.config, none),
ActionConfig = proplists:get_value(action, WebApp#webapp.config, []),
StaticConfig = proplists:get_value(static, WebApp#webapp.config, none),
Static = get_static(StaticConfig),
Options = [
{resource_server, ResourceServer},
{context, Context},
{session_manager, SessionManager},
{static, Static}
],
ActionPath = action_path(ActionConfig, [], []),
PathsWithTemplate = add_template(TemplateConfig, Context, Options, []),
PathsWithAction = add_action(ActionPath, Context, Options, PathsWithTemplate),
PathsWithStatic = add_static(StaticConfig, Context, PathsWithAction),
Sorted = lists:sort(fun({A, _, _}, {B, _, _}) -> sort_paths(A, B) end, PathsWithStatic),
get_web_app_config(T, lists:append(Sorted, Paths)).
get_static(none) -> "";
get_static(Config) ->
Path = proplists:get_value(path, Config, "/"),
remove_slashs(Path) ++ "/".
action_path([{Filter, SubConfig}|T], Path, Output) when is_atom(Filter) andalso is_list(SubConfig) ->
NewOutput = action_path(SubConfig, [Filter|Path], []),
action_path(T, Path, Output ++ NewOutput);
action_path([{ActionContext, Action}|T], Path, Output) ->
CallbackList = lists:reverse([Action|Path]),
action_path(T, Path, [{ActionContext, CallbackList}|Output]);
action_path([], _Path, Output) -> Output.
sort_paths(A, B) ->
{UA, QA} = fix_path(A),
{UB, QB} = fix_path(B),
if UA < UB -> true;
UA == UB -> QA > QB;
true -> false
end.
fix_path(Path) ->
case string:rstr(Path, "[...]") of
0 -> {Path, ""};
Len ->
Url = string:substr(Path, 1, Len - 1),
{Url, "[...]"}
end.
add_template(none, _Context, _Options, Paths) -> Paths;
add_template(TemplateConfig, Context, Options, Paths) ->
TemplatePrefix = proplists:get_value(prefix, TemplateConfig, "page"),
TopPage = proplists:get_value(top_page, TemplateConfig, "index"),
NPaths = add_top_page(TopPage, Context, Options, Paths),
lists:append([
{get_template_match(TemplatePrefix, Context), kb_cowboy_template, Options}
], NPaths).
add_top_page(none, _Context, _Options, Paths) -> Paths;
add_top_page(TopPage, Context, Options, Paths) ->
lists:append([
{Context, kb_cowboy_toppage, [{top_page, TopPage}] ++ Options}
], Paths).
get_template_match(TemplatePrefix, Context) ->
Context ++ remove_slashs(TemplatePrefix) ++ "/[...]".
add_action([], _Context, _Options, Paths) -> Paths;
add_action([{ActionPrefix, CallbackList}|T], Context, Options, Paths) ->
NakedActionPrefix = remove_slashs(ActionPrefix),
NPaths = lists:append([
{get_action_match(NakedActionPrefix, Context), kb_cowboy_action, [{callback_list, CallbackList}, {action_prefix, NakedActionPrefix}] ++ Options}
], Paths),
add_action(T, Context, Options, NPaths).
get_action_match(ActionPrefix, Context) ->
Context ++ ActionPrefix ++ "/[...]".
add_static(none, _Context, Paths) -> Paths;
add_static(StaticConfig, Context, Paths) ->
Path = proplists:get_value(path, StaticConfig, "/"),
Options = [{mimetypes, cow_mimetypes, all}],
Config = case lists:keyfind(priv_dir, 1, StaticConfig) of
{_, App, Dir} -> {priv_dir, App, Dir, Options};
false ->
Dir = proplists:get_value(dir, StaticConfig, "./static"),
{dir, Dir, Options}
end,
lists:append([
{get_static_match(Path, Context), cowboy_static, Config}
], Paths).
get_static_match(Path, Context) ->
case Path of
"/" -> string:concat(Context, "[...]");
_ -> Context ++ remove_slashs(Path) ++ "/[...]"
end.
get_context("/") -> "/";
get_context(WebContext) ->
"/" ++ remove_slashs(WebContext) ++ "/".