Current section
Files
Jump to
Current section
Files
lib/medic/checks/postgres.ex
defmodule Medic.Checks.Postgres do
@moduledoc """
Checks that Postgres is running, and is set up correctly.
## Examples
{Check.Postgres, :running?},
{Check.Postgres, :correct_version_running?},
{Check.Postgres, :role_exists?},
{Check.Postgres, :correct_data_directory?},
{Check.Postgres, :database_exists?, ["my_db"]}
## Environment variables
Medic uses `psql` in order to connect to Postgres for its checks. When using
a non-standard configuration, such as database port, consider setting environment
variables such as `PGPORT` (in `.envrc` or `.envrc.local`) to configure `psql` if
possible.
These environment variables are documented in
[libpq's documentation](https://www.postgresql.org/docs/current/libpq-envars.html).
"""
@default_data_dir "./priv/postgres/data"
@doc """
Checks that the named database exists in the running Postgres instance.
## Usage
{Medic.Checks.Postgres, :database_exists?, ["my_db_dev"]}
{Medic.Checks.Postgres, :database_exists?, ["my_db_dev", username: "postgres"]}
"""
@spec database_exists?(binary()) :: Medic.Check.check_return_t()
def database_exists?(database_name, opts \\ []) do
{:ok, found_databases} = databases(List.wrap(opts))
if database_name in found_databases,
do: :ok,
else: {:error, "#{database_name} not found in #{inspect(found_databases)}", "mix ecto.setup"}
end
@doc """
Checks that the running Postgres database matches the version defined
in ASDF's `.tool-versions` file.
Options:
* `remedy`: the remedy as a string
"""
@spec correct_version_running?() :: Medic.Check.check_return_t()
def correct_version_running?(opts \\ []) do
with {:ok, project_version} <- get_project_version(),
{:ok, running_version} <- get_running_version() do
if project_version == running_version,
do: :ok,
else: {
:error,
"running database version #{running_version} does not match project version #{project_version}",
Keyword.get(opts, :remedy, "bin/dev/db-restart")
}
else
{:error, _} ->
{:error, "Unable to determine desired or running postgres. Please check that the desired version is running.", "# remediate"}
end
end
@doc """
Checks that a user has been created in the running instance. This check defaults
to the username `postgres` if not explicitly given.
## Usage
{Medic.Checks.Postgres, :role_exists?}
{Medic.Checks.Postgres, :role_exists?, ["postgres"]}
"""
@spec role_exists?(binary()) :: Medic.Check.check_return_t()
def role_exists?(username \\ "postgres") do
System.cmd("psql", ["-A", "-c", "\\du", username], stderr_to_stdout: true)
|> case do
{output, 0} ->
if output =~ "postgres",
do: :ok,
else: {:error, "postgres role does not exist", "createuser -s #{username} -U \$USER"}
{output, _} ->
{:error, output, "# start postgres"}
end
end
@doc """
Checks that the running instance of Postgres has the expected data directory.
If run with no arguments, this expects that the data directory is located at `#{@default_data_dir}`
within the current project.
If run with one argument, the argument can be:
* A path to the data directory, or
* A keyword list with one or more of the following keys:
* `data_directory`: the path to the data directory
* `remedy`: the remedy as a string
* `username`: username to use when calling psql
## Usage
{Medic.Checks.Postgres, :correct_data_directory?}
{Medic.Checks.Postgres, :correct_data_directory?, ["/path/to/data/directory"]}
{Medic.Checks.Postgres, :correct_data_directory?, [data_directory: "/path/to/data/directory", username: "postgres"]}
{Medic.Checks.Postgres, :correct_data_directory?, [data_directory: "/path/to/data/directory", remedy: "bin/dev/db-restart"]}
"""
@spec correct_data_directory?(Path.t() | list()) :: Medic.Check.check_return_t()
def correct_data_directory?(path_or_opts \\ @default_data_dir)
def correct_data_directory?(path) when is_binary(path) do
correct_data_directory?(data_directory: path)
end
def correct_data_directory?(opts) when is_list(opts) do
expected_data_dir = opts |> Keyword.get(:data_directory, @default_data_dir) |> Path.expand()
{actual_data_dir, 0} = System.cmd("psql", ["-tA", "-c", "SHOW data_directory;" | psql_opts(opts)], stderr_to_stdout: true)
if String.trim(actual_data_dir) == expected_data_dir do
:ok
else
{
:error,
"expected data directory to be #{expected_data_dir} but it was #{actual_data_dir}",
Keyword.get(opts, :remedy, "# start postgres from #{expected_data_dir}")
}
end
end
@doc """
Checks whether Postgres is running, by attempting to list all databases.
Options:
* `remedy`: the remedy as a string
* `username`: username to use when calling psql
"""
@spec running?(list()) :: Medic.Check.check_return_t()
def running?(opts \\ []) do
case databases(opts) do
{:ok, _list} -> :ok
{:error, output} -> {:error, output, Keyword.get(opts, :remedy, "# start postgres")}
end
end
# # #
defp databases(opts) do
case System.cmd("psql", ["-l", "-x" | psql_opts(opts)], stderr_to_stdout: true) do
{output, 0} ->
{:ok, Regex.scan(~r"^Name\s+\| (\w+)\s*$"m, output) |> Enum.map(&List.last/1)}
{output, _} ->
{:error, output}
end
end
defp get_project_version do
case System.cmd("asdf", ["current", "postgres"]) do
{output, 0} ->
output
|> String.split(" ", trim: true)
|> Enum.at(1)
|> ok()
{output, _} ->
{:error, output}
end
end
defp get_running_version do
case System.cmd("psql", ["--version"], stderr_to_stdout: true) do
{output, 0} ->
output
|> String.split(" ", trim: true)
|> Enum.at(2)
|> String.trim()
|> ok()
{output, _} ->
{:error, output}
end
end
defp ok(result) do
{:ok, result}
end
defp psql_opts(opts) do
case Keyword.fetch(opts, :username) do
{:ok, username} -> ["-U", username]
:error -> ["-U", "postgres"]
end
end
end