diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..524cd59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# general +log +/_build/ +/_checkouts/ +*~ +erl_crash.dump +rebar3.crashdump +.tags* +*.sublime-workspace +.edts +.DS_Store +/.idea/ +*.beam +/test/log/ + +tags +.image.dev +bin diff --git a/README.md b/README.md index 0ef0f12..575791b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # epg_connector -Postgres connector fot Erlang +Postgres connector for Erlang diff --git a/config/sys.config b/config/sys.config new file mode 100644 index 0000000..e12457d --- /dev/null +++ b/config/sys.config @@ -0,0 +1,28 @@ +[ + {epg_connector, [ + {databases, #{ + default_db => #{ + host =>"127.0.0.1", + port => 5432, + database => "db_name", + username => "postgres", + password => "postgres" + } + }}, + {pools, #{ + default_pool => #{ + database => default_db, + size => 10 + } + }}, + %% Must specified for umbrella application + {vault_token_path, "/var/run/secrets/kubernetes.io/serviceaccount/token"}, + {vault_role, "epg_connector"}, + {vault_key_pg_creds, "epg_connector/pg_creds"} + ]}, + + {canal, [ + {url, "http://vault:8200"}, + {engine, kvv2} + ]} +]. \ No newline at end of file diff --git a/rebar.config b/rebar.config new file mode 100644 index 0000000..be80929 --- /dev/null +++ b/rebar.config @@ -0,0 +1,13 @@ +{erl_opts, [debug_info]}. +{deps, [ + jsx, + {canal, {git, "https://github.com/valitydev/canal", {branch, master}}}, + {epgsql, {git, "https://github.com/epgsql/epgsql.git", {tag, "4.7.1"}}}, + {epgsql_pool, {git, "https://github.com/wgnet/epgsql_pool", {branch, "master"}}}, + {herd, {git, "https://github.com/wgnet/herd.git", {tag, "1.3.4"}}} +]}. + +{shell, [ + % {config, "config/sys.config"}, + {apps, [epg_connector]} +]}. diff --git a/src/epg_connector.app.src b/src/epg_connector.app.src new file mode 100644 index 0000000..f6fc3f2 --- /dev/null +++ b/src/epg_connector.app.src @@ -0,0 +1,18 @@ +{application, epg_connector, + [{description, "An OTP application"}, + {vsn, "0.1.0"}, + {registered, []}, + {mod, {epg_connector_app, []}}, + {applications, + [kernel, + stdlib, + canal, + epgsql, + epgsql_pool + ]}, + {env,[]}, + {modules, []}, + + {licenses, ["MIT"]}, + {links, []} + ]}. diff --git a/src/epg_connector_app.erl b/src/epg_connector_app.erl new file mode 100644 index 0000000..b2ec801 --- /dev/null +++ b/src/epg_connector_app.erl @@ -0,0 +1,101 @@ +%%%------------------------------------------------------------------- +%% @doc epg_connector public API +%% @end +%%%------------------------------------------------------------------- + +-module(epg_connector_app). + +-behaviour(application). + +-define(VAULT_TOKEN_PATH, "/var/run/secrets/kubernetes.io/serviceaccount/token"). +-define(VAULT_ROLE, "api-key-mgmt-v2"). +-define(VAULT_KEY_PG_CREDS, "api-key-mgmt-v2/pg_creds"). + +-export([start/2, stop/1]). + +start(_StartType, _StartArgs) -> + Databases = application:get_env(epg_connector, databases, #{}), + ok = maybe_set_secrets(Databases), + Pools = application:get_env(epg_connector, pools, #{}), + ok = start_pools(Pools, Databases), + epg_connector_sup:start_link(). + +stop(_State) -> + ok. + +%% internal functions + +start_pools(Pools, Databases) -> + maps:fold( + fun(PoolName, Opts, _Acc) -> + #{ + database := DB, + size := Size + } = Opts, + DbParams = maps:get(DB, Databases), + {ok, _} = epgsql_pool:start(PoolName, Size, Size, DbParams), + ok + end, + ok, + Pools + ). + +maybe_set_secrets(Databases) -> + TokenPath = application:get_env(epg_connector, vault_token_path, ?VAULT_TOKEN_PATH), + try vault_client_auth(TokenPath) of + ok -> + Key = unicode:characters_to_binary( + application:get_env(epg_connector, vault_key_pg_creds, ?VAULT_KEY_PG_CREDS) + ), + set_secrets(canal:read(Key), Databases); + Error -> + logger:error("can`t auth vault client with error: ~p", [Error]), + skip + catch + _:_ -> + logger:error("catch exception when auth vault client"), + skip + end, + ok. + +vault_client_auth(TokenPath) -> + case read_maybe_linked_file(TokenPath) of + {ok, Token} -> + Role = unicode:characters_to_binary(application:get_env(epg_connector, vault_role, ?VAULT_ROLE)), + canal:auth({kubernetes, Role, Token}); + Error -> + Error + end. + +read_maybe_linked_file(MaybeLinkName) -> + case file:read_link(MaybeLinkName) of + {error, enoent} = Result -> + Result; + {error, einval} -> + file:read_file(MaybeLinkName); + {ok, Filename} -> + file:read_file(maybe_expand_relative(MaybeLinkName, Filename)) + end. + +maybe_expand_relative(BaseFilename, Filename) -> + filename:absname_join(filename:dirname(BaseFilename), Filename). + +set_secrets({ok, #{<<"pg_creds">> := #{<<"pg_user">> := PgUser, <<"pg_password">> := PgPassword}}}, Databases) -> + logger:info("postgres credentials successfuly read from vault (as json)"), + NewDbConfig = maps:fold(fun(DbName, ConnOpts, Acc) -> + Acc#{ + DbName => ConnOpts#{ + username => unicode:characters_to_list(PgUser), + password => unicode:characters_to_list(PgPassword) + } + } + end, #{}, Databases), + application:set_env(epg_connector, databases, NewDbConfig), + ok; +set_secrets({ok, #{<<"pg_creds">> := PgCreds}}, Databases) -> + logger:info("postgres credentials successfuly read from vault (as string)"), + set_secrets({ok, #{<<"pg_creds">> => jsx:decode(PgCreds, [return_maps])}}, Databases); +set_secrets(Error, _Databases) -> + logger:error("can`t read postgres credentials from vault with error: ~p", [Error]), + skip. + diff --git a/src/epg_connector_sup.erl b/src/epg_connector_sup.erl new file mode 100644 index 0000000..5dc8f2b --- /dev/null +++ b/src/epg_connector_sup.erl @@ -0,0 +1,26 @@ +%%%------------------------------------------------------------------- +%% @doc epg_connector top level supervisor. +%% @end +%%%------------------------------------------------------------------- + +-module(epg_connector_sup). + +-behaviour(supervisor). + +-export([start_link/0]). + +-export([init/1]). + +-define(SERVER, ?MODULE). + +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +init([]) -> + SupFlags = #{strategy => one_for_all, + intensity => 0, + period => 1}, + ChildSpecs = [], + {ok, {SupFlags, ChildSpecs}}. + +%% internal functions