-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
308 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
21
lib/kube_probex/exceptions/ecto/database_not_created_error.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
28
lib/kube_probex/exceptions/ecto/pending_migrations_error.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |