Skip to content

Commit

Permalink
feat: add readines probe plug
Browse files Browse the repository at this point in the history
  • Loading branch information
JVZELLER committed Jan 23, 2025
1 parent d4a1213 commit a9f890c
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 5 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,24 @@ mix deps.get

## Usage

### Adding the Plug to Your Phoenix Router
### Adding the Plug to Your Phoenix Endpoint or Router

To use `kube_probex`, add it to your Phoenix router as a plug:
To use `kube_probex`, add it to your Phoenix endpoint or router as a plug:

```elixir
# lib/my_app_web/endpoint.ex

defmodule MyAppWeb.Router do
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app_web

plug KubeProbex.Plug.Liveness, path: ~w(/_health /_healthz)
plug KubeProbex.Plug.Readiness, path: ~w(/_ready /_readyz), otp_apps: [:my_app_web]
end
```

This will expose a liveness probe endpoint at `/_health` and `/_healthz` paths.
This will expose:
- A liveness probe endpoint at `/_health` and `/_healthz` paths.
- A readiness probe endpoint at `/_ready` and `/_readyz` paths.

### Customizing Probe Checks

Expand Down
118 changes: 118 additions & 0 deletions lib/kube_probex/check/ecto_ready.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
if {:module, Ecto.Migrator} == Code.ensure_compiled(Ecto.Migrator) do
defmodule KubeProbex.Check.EctoReady do
@moduledoc """
Provides the default implementation for Kubernetes readiness probes check using Ecto.
This module defines a readiness probe handler that ensures database health by performing
the following checks:
1. **Pending Migrations**: Checks if there are any pending migrations for the configured repositories.
2. **Database Storage**: If there are no migrations defined, verifies if the database storage is properly created.
If any of these checks fail, the module raises appropriate exceptions to indicate the issue:
- `KubeProbex.Exceptions.Ecto.PendingMigrationsError` is raised if there are pending migrations.
- `KubeProbex.Exceptions.Ecto.DatabaseNotCreatedError` is raised if the database is not created.
When all checks pass, the module returns an HTTP response with the following attributes:
- **Status**: `200 OK`
- **Content-Type**: `application/json`
- **Body**: `{"status": "ready"}`
This indicates that the application is ready to serve traffic.
This module serves as the default adapter for the `KubeProbex.Check.Readiness` behaviour. It is
primarily used to ensure that Kubernetes readiness probes respond with a "ready" status only when
the application database is in a healthy state.
## Usage
This module is used internally by the `KubeProbex` library and should not be called directly.
Instead, ensure your application specifies the required `:ecto_repos` configuration.
### Example
```elixir
config :my_app, :ecto_repos, [MyApp.Repo]
```
## Options
- `:otp_apps` - A list of OTP applications whose Ecto repositories should be checked. Each
application must define its Ecto repositories under the `:ecto_repos` configuration key. This option
should be passed when setting the plug.
### Exemple
```elixir
defmodule MyAppWeb.Router do
use Phoenix.Endpoint, otp_app: :my_app_web
plug KubeProbex.Plug.Readiness, path: ~w(/_ready /_readyz), otp_apps: [:my_app_web]
end
````
"""

@behaviour KubeProbex.Check.Readiness

alias KubeProbex.Exceptions.Ecto.DatabaseNotCreatedError
alias KubeProbex.Exceptions.Ecto.PendingMigrationsError

alias Plug.Conn

@impl true
def check(%Conn{} = conn, opts) do
repos =
opts
|> Keyword.get(:otp_apps, [])
|> List.wrap()
|> Enum.flat_map(&Application.get_env(&1, :ecto_repos, []))

for repo <- repos, Process.whereis(repo) do
check_pending_migrations!(repo, opts) || check_storage_up!(repo)
end

conn
|> Conn.put_resp_content_type("application/json")
|> Conn.send_resp(200, ~s({"status": "ready"}))
end

defp check_storage_up!(repo) do
adapter = repo.__adapter__()
repo_config = repo.config()

repo_config
|> adapter.storage_status()
|> case do
:down ->
raise DatabaseNotCreatedError, repo: repo

{:error, reason} ->
raise "[KubeProbex] Readiness probe failed for repo #{inspect(repo)} due to unexpected reason: #{inspect(reason)}"

:up ->
{:ok, :storage_up}
end
end

defp check_pending_migrations!(repo, _opts) do
migrations = Ecto.Migrator.migrations(repo)
empty? = Enum.empty?(migrations)

pending_migrations? =
Enum.any?(migrations, fn {status, _version, _migration} -> status == :down end)

cond do
empty? ->
nil

pending_migrations? ->
raise PendingMigrationsError, repo: repo

:neither_empty_nor_pending_migrations ->
{:ok, :all_migrations_up}
end
end
end
end
3 changes: 2 additions & 1 deletion lib/kube_probex/check/liveness.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ defmodule KubeProbex.Check.Liveness do
By default, the `Heartbeat` module is used to handle liveness checks. However, you
can override this behaviour by configuring a custom module in your application
configuration under the `:kube_probex` key:
```elixir
config :kube_probex, :liveness_check, MyCustomLivenessCheck
config :kube_probex, :liveness_check, MyCustomLivenessCheck
```
"""

Expand Down
73 changes: 73 additions & 0 deletions lib/kube_probex/check/readiness.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
defmodule KubeProbex.Check.Readiness do
@moduledoc """
Defines the behaviour for implementing Kubernetes HTTP readiness checks.
This module specifies a contract for handling readiness probe requests in a Kubernetes environment.
Readiness checks are used by Kubernetes to determine if the application is ready to serve traffic.
## Behaviour
To implement a custom readiness check, a module must define the `check/2` callback, which:
- Processes the HTTP request represented by a `Plug.Conn`.
- Determines and sets the appropriate HTTP response status, headers, and body.
## Default Implementation
By default, the `KubeProbex.Check.EctoReady` module is used to handle readiness checks.
## Custom Readiness Checks
You can override the default readiness check implementation by configuring your custom module in your application
under the `:kube_probex` key:
```elixir
config :kube_probex, :readiness_check, MyCustomReadinessCheck
```
Your custom module must implement the `KubeProbex.Check.Readiness` behaviour by defining the `check/2` function.
### Example
A basic custom implementation:
```elixir
defmodule MyCustomReadinessCheck do
@behaviour KubeProbex.Check.Readiness
alias Plug.Conn
@impl true
def check(conn, _opts) do
conn
|> Conn.put_resp_content_type("application/json")
|> Conn.send_resp(200, ~s({"status": "ready"}))
end
end
```
"""

@default_adapter KubeProbex.Check.EctoReady

alias Plug.Conn

@callback check(Conn.t(), keyword) :: Conn.t()

@doc """
Executes the readiness check logic.
This function processes an HTTP request for a readiness probe. It takes a `Plug.Conn`
struct and a list of options. The implementation determines the response status,
content type, and body.
## Parameters
- `conn` - The `Plug.Conn` representing the HTTP request.
- `opts` - A list of options provided for the readiness check plug.
"""
def check(%Conn{} = conn, opts), do: adapter().check(conn, opts)

defp adapter do
Application.get_env(:kube_probex, :readiness_check) || @default_adapter
end
end
21 changes: 21 additions & 0 deletions lib/kube_probex/exceptions/ecto/database_not_created_error.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule KubeProbex.Exceptions.Ecto.DatabaseNotCreatedError do
@moduledoc """
An exception raised when a database storage is not created for a specific Ecto repository.
This exception is used by the `KubeProbex.Check.EctoReady` module during readiness checks
to signal that the required database storage has not been initialized. It provides
a clear error message to guide developers in resolving the issue.
## Attributes
- `repo` - The Ecto repository for which the database storage is missing.
"""

defexception [:repo]

@impl Exception
def message(%__MODULE__{repo: repo}) do
"[KubeProbex] the storage is not created for repo: #{inspect(repo)}. " <>
"Try running `mix ecto.create` in the command line to create it"
end
end
28 changes: 28 additions & 0 deletions lib/kube_probex/exceptions/ecto/pending_migrations_error.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule KubeProbex.Exceptions.Ecto.PendingMigrationsError do
@moduledoc """
An exception raised when there are pending database migrations for a specific Ecto repository.
This exception is used by the `KubeProbex.Check.EctoReady` module during readiness checks
to indicate that a repository requires migrations to be run before it can be considered ready.
It provides a clear error message to guide developers in resolving the issue.
## Example
This exception can be triggered if the `Ecto.Repo` has unapplied migrations:
```elixir
raise KubeProbex.Exceptions.Ecto.PendingMigrationsError, repo: MyApp.Repo
```
## Attributes
- `repo` - The Ecto repository that has pending migrations.
"""
defexception [:repo]

@impl Exception
def message(%__MODULE__{repo: repo}) do
"[KubeProbex] there are pending migrations for repo: #{inspect(repo)}. " <>
"Try running `mix ecto.migrate` in the command line to migrate it"
end
end
59 changes: 59 additions & 0 deletions lib/kube_probex/plug/readiness.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
if {:module, Plug} == Code.ensure_compiled(Plug) do
defmodule KubeProbex.Plug.Readiness do
@moduledoc """
A plug for handling Kubernetes HTTP readiness probe requests.
This module integrates with Phoenix or Plug applications to define a readiness probe
endpoint. It validates the incoming request's path and executes the configured readiness
check logic.
## Default Behavior
- The default path for readiness probes is `"/readyz"`.
- The readiness check is performed using the `KubeProbex.Check.Readiness` behaviour implemented
by `KubeProbex.Check.EctoReady` by default. Check its documentation for more details on how to use it.
- If the incoming request's path does not match the expected path, the request is passed
through unaltered.
## Configuration
You can customize the path for the readiness probe by providing a `:path` option
when configuring this plug. If no custom path is provided, the default path `"/readyz"`
will be used.
## Example
Add the readiness plug to your router or endpoint to define the readiness probe:
```elixir
defmodule MyAppWeb.Router do
use Phoenix.Endpoint, otp_app: :my_app_web
plug KubeProbex.Plug.Readiness, path: ~w(/_ready /_readyz), otp_apps: [:my_app_web]
end
```
"""

@behaviour Plug

@default_path "/readyz"

alias KubeProbex.Check.Readiness
alias KubeProbex.Plug.PathValidator
alias Plug.Conn

@impl true
def init(opts), do: opts

@impl true
def call(%Conn{} = conn, opts) do
if PathValidator.valid_path?(conn, opts, @default_path) do
conn
|> Readiness.check(opts)
|> Conn.halt()
else
conn
end
end
end
end

0 comments on commit a9f890c

Please sign in to comment.