LoginSignup
9

More than 5 years have passed since last update.

マクロを使ってPostgreSQLの接続情報を取得する。

Posted at

Elixir Advent Calendar 2014 21日目。
PostgreSQLに接続する際に使用するpsqlコマンドが利用している、.pgpass.pg_service.confをElixirで読み込んで接続情報を取得する方法を書きたいと思います。

このような事をやりたかった理由は、Elixirのプロジェクト内に接続情報を保持したくなかったのと、psqlと同じ情報を利用したかったためです。

やりたい事

  • .pg_service.confに記述されているサービス名を引数に渡すと、.pgpass/.pg_service.confから接続情報を取り出す。

準備

$HOMEに.pgpass.pg_service.confを作成しときます。
設定内容は以下の通り。

.pgpass
# hostname:port:database:username:password
*:*:admindb:*:passwd_admin
*:*:userdb:*:passwd_user
.pg_service.conf
[srv_user]
host=awssrv.example.com
port=5432
dbname=userdb
user=admin

[srv_admin]
host=awssrv.example.com
port=5432
dbname=admindb
user=admin

前提条件

  • 接続情報に対してデフォルト値を設けていないので、.pg_service.confにはpassword以外のすべての項目を設定する。
  • マクロを使うのは.pgpassの情報を取得する部分のみ。
  • .pgpassのpasswordには":"は入っていないこと。(エスケープに対応してないです。)

使用するライブラリ

.pg_service.confはini形式なので、読み込みにeconfigを使用します。

コードを書いてみる

.pg_service.conf読み込み部分

.pg_service.confに設定情報が無い場合、Application.get_envmix.exsから取得してます。

lib/pg/service.ex
defmodule Pg.Service do
  def init do
    :econfig.register_config(:pg_service, [config_file], [])
    :econfig.subscribe(:pg_service)
  end

  def connection_data(dbtype) do
    case :econfig.get_value(:pg_service, dbtype) do
      [] ->
        Application.get_env(:define_on_compile_phase, List.to_atom dbtype)
      _ ->
        [
          hostname: :econfig.get_value(:pg_service, dbtype, 'host'),
          username: :econfig.get_value(:pg_service, dbtype, 'user'),
          password: :econfig.get_value(:pg_service, dbtype, 'password'),
          database: :econfig.get_value(:pg_service, dbtype, 'dbname'),
          port:     List.to_integer(:econfig.get_value(:pg_service, dbtype, 'port')),
        ]
    end
  end

  defp config_file do
    String.to_char_list(System.user_home) ++ '/.pg_service.conf'
  end
end

.pgpassのパスワード取得のパターンマッチングに使用するStruct

lib/pg/pass_struct.ex
defmodule Pg.PassStruct do
  defstruct [hostname: nil, port: nil, database: nil, username: nil, password: nil]
end

.pgpass読み込み部分

このファイルの中には、Pg.ReadPgpassPg.Passwordの二つのmoduleが含まれています。
ファイルを分けなかった理由は、コンパイル時にPg.Passwordの内部でマクロが実行されPg.ReadPgpassに依存している為です。
(ファイルを分割したら、コンパイルエラーになります。)

マクロを使用しているのは、Pg.Password moduleで、
Pg.PassStructを使ってパターンマッチング出来るget関数を定義して、マッチするとパスワードを返す様になってます。

lib/pg/read_pgpass.ex
defmodule Pg.ReadPgpass do
  def get do
    {:ok, bin} = File.read(config_file)
    bin |> String.split("\n") |> resolution
  end

  defp config_file do
    String.to_char_list(System.user_home) ++ '/.pgpass'
  end

  defp resolution(config_arr), do: resolution(config_arr, [])
  defp resolution([], acc), do: acc
  defp resolution([""|t], acc), do: resolution(t, acc)
  defp resolution([<<"#", _rest::binary>>|t], acc), do: resolution(t, acc)
  defp resolution([h|t], acc) do
    ltuple = String.split(h, ":") |> List.to_tuple
    {_, r} = {ltuple, %Pg.PassStruct{}} |>
      set_hostname |>
      set_port |>
      set_database |>
      set_username |>
      set_password
    resolution(t, [r|acc])
  end

  defp set_hostname({t, r}) do
    case (t |> elem 0) do
      "*" -> {t, r}
      v -> {t, Map.put(r, :hostname, v |> String.to_char_list)}
    end
  end
  defp set_port({t, r}) do
    case (t |> elem 1) do
      "*" -> {t, r}
      v -> {t, Map.put(r, :port, v |> String.to_integer)}
    end
  end
  defp set_database({t, r}) do
    case (t |> elem 2) do
      "*" -> {t, r}
      v -> {t, Map.put(r, :database, v |> String.to_char_list)}
    end
  end
  defp set_username({t, r}) do
    case (t |> elem 3) do
      "*" -> {t, r}
      v -> {t, Map.put(r, :username, v |> String.to_char_list)}
    end
  end
  defp set_password({t, r}) do
    case (t |> elem 4) do
      "*" -> {t, r}
      v -> {t, Map.put(r, :password, v |> String.to_char_list)}
    end
  end
end

defmodule Pg.Password do
  list = Pg.ReadPgpass.get

  def list_to_struct(list) do
    %Pg.PassStruct{
      hostname: list[:hostname],
      port:     list[:port],
      database: list[:database],
      username: list[:username],
    }
  end

  Enum.each list, fn(pgpass) ->
    opt = Keyword.new([hostname: pgpass.hostname, port: pgpass.port, database: pgpass.database, username: pgpass.username])
    opt = Enum.reject opt, fn(y) ->
      case y do
        {_, nil} -> true
        _ -> false
      end
    end
    password = pgpass.password

    def get(%Pg.PassStruct{unquote_splicing(opt)}=_val) do
      unquote(password)
    end
  end
end

動作確認

iex
% iex -S mix
Erlang/OTP 17 [erts-6.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Interactive Elixir (1.1.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Pg.Service.init
true
iex(2)> connection_info = Pg.Service.connection_data("srv_admin")
[hostname: 'awssrv.example.com', username: 'admin', password: :undefined,
 database: 'admindb', port: 5432]
iex(3)> password = connection_info |> Pg.Password.list_to_struct |> Pg.Password.get
'passwd_admin'
iex(4)> Keyword.put connection_info, :password, password
[password: 'passwd_admin', hostname: 'awssrv.example.com', username: 'admin',
 database: 'admindb', port: 5432]
iex(5)>

まとめ

  • 気をつけないといけないのは、.pgpassを更新してもコンパイルしたモジュールは古いパスワードを返します。 更新した場合は、mix do clean, compileを行う必要があります。
  • Stage環境/本番環境できっちり.pgpass.pg_service.confを使い分けている場合、本番環境でコンパイルしないといけません。Stage環境で作成したモジュールを本番に持って行っても、.pgpassの情報はStage環境のママです。

あと同じ考え方で、AWSのキー/証明書等をを取り込ませたりする事も出来ると思います。
もっと最適化したいな。

明日は @keithseahus さんです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9