- add tests for QueryHelpers.sanitize_like/1 (%, _, \ escaping) - fix nesting depth in store_check_result, handle_check_state_change, graphql check resolver, checks controller, and ping executor - fix cyclomatic complexity in build_check_protobuf by extracting check_type_config/2 function clauses - fix formatting in api_docs_html template Reviewed-on: graham/towerops-web#56
202 lines
5.5 KiB
Elixir
202 lines
5.5 KiB
Elixir
defmodule ToweropsWeb.Api.V1.ChecksController do
|
|
@moduledoc """
|
|
API controller for managing service checks (HTTP, TCP, DNS, ping).
|
|
|
|
All endpoints require API token authentication and operations are scoped
|
|
to the organization associated with the token. Only service check types
|
|
(http, tcp, dns, ping) are exposed — SNMP checks are managed internally.
|
|
"""
|
|
use ToweropsWeb, :controller
|
|
|
|
import ToweropsWeb.Api.ErrorHelpers, only: [translate_errors: 1]
|
|
|
|
alias Towerops.Monitoring
|
|
alias Towerops.Monitoring.Check
|
|
alias ToweropsWeb.ScopedResource
|
|
|
|
@service_check_types ~w(http tcp dns ping)
|
|
|
|
@doc """
|
|
GET /api/v1/checks
|
|
|
|
Lists service checks for the authenticated organization.
|
|
Optional query params: `check_type`, `device_id`.
|
|
"""
|
|
def index(conn, params) do
|
|
organization_id = conn.assigns.current_organization_id
|
|
|
|
opts =
|
|
[]
|
|
|> maybe_add_opt(:device_id, params["device_id"])
|
|
|> maybe_add_opt(:check_type, params["check_type"])
|
|
|
|
checks =
|
|
organization_id
|
|
|> Monitoring.list_checks(opts)
|
|
|> Enum.filter(&(&1.check_type in @service_check_types))
|
|
|> Enum.map(&format_check/1)
|
|
|
|
json(conn, %{checks: checks})
|
|
end
|
|
|
|
@doc """
|
|
POST /api/v1/checks
|
|
|
|
Creates a new service check for the authenticated organization.
|
|
"""
|
|
def create(conn, %{"check" => check_params}) do
|
|
organization_id = conn.assigns.current_organization_id
|
|
|
|
check_type = Map.get(check_params, "check_type", "")
|
|
|
|
if check_type in @service_check_types do
|
|
attrs = Map.put(check_params, "organization_id", organization_id)
|
|
create_and_respond(conn, attrs)
|
|
else
|
|
conn
|
|
|> put_status(:bad_request)
|
|
|> json(%{error: "Invalid check type '#{check_type}'. Must be one of: #{Enum.join(@service_check_types, ", ")}"})
|
|
end
|
|
end
|
|
|
|
def create(conn, _params) do
|
|
conn
|
|
|> put_status(:bad_request)
|
|
|> json(%{error: "Missing 'check' parameter"})
|
|
end
|
|
|
|
defp create_and_respond(conn, attrs) do
|
|
case Monitoring.create_check(attrs) do
|
|
{:ok, check} ->
|
|
if check.enabled, do: Monitoring.schedule_check(check)
|
|
|
|
conn
|
|
|> put_status(:created)
|
|
|> json(format_check(check))
|
|
|
|
{:error, %Ecto.Changeset{} = changeset} ->
|
|
conn
|
|
|> put_status(:unprocessable_entity)
|
|
|> json(%{errors: translate_errors(changeset)})
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
GET /api/v1/checks/:id
|
|
|
|
Gets a single check by ID.
|
|
"""
|
|
def show(conn, %{"id" => id}) do
|
|
organization_id = conn.assigns.current_organization_id
|
|
|
|
case ScopedResource.fetch(Check, id, organization_id) do
|
|
{:ok, check} ->
|
|
json(conn, format_check(check))
|
|
|
|
{:error, :forbidden} ->
|
|
conn
|
|
|> put_status(:forbidden)
|
|
|> json(%{error: "Access denied to this check"})
|
|
|
|
{:error, :not_found} ->
|
|
conn
|
|
|> put_status(:not_found)
|
|
|> json(%{error: "Check not found"})
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
PATCH /api/v1/checks/:id
|
|
|
|
Updates a check.
|
|
"""
|
|
def update(conn, %{"id" => id, "check" => check_params}) do
|
|
organization_id = conn.assigns.current_organization_id
|
|
|
|
case ScopedResource.fetch(Check, id, organization_id) do
|
|
{:ok, check} ->
|
|
case Monitoring.update_check(check, check_params) do
|
|
{:ok, updated_check} ->
|
|
json(conn, format_check(updated_check))
|
|
|
|
{:error, %Ecto.Changeset{} = changeset} ->
|
|
conn
|
|
|> put_status(:unprocessable_entity)
|
|
|> json(%{errors: translate_errors(changeset)})
|
|
end
|
|
|
|
{:error, :forbidden} ->
|
|
conn
|
|
|> put_status(:forbidden)
|
|
|> json(%{error: "Access denied to this check"})
|
|
|
|
{:error, :not_found} ->
|
|
conn
|
|
|> put_status(:not_found)
|
|
|> json(%{error: "Check not found"})
|
|
end
|
|
end
|
|
|
|
def update(conn, _params) do
|
|
conn
|
|
|> put_status(:bad_request)
|
|
|> json(%{error: "Missing 'check' parameter"})
|
|
end
|
|
|
|
@doc """
|
|
DELETE /api/v1/checks/:id
|
|
|
|
Deletes a check and cancels any scheduled jobs.
|
|
"""
|
|
def delete(conn, %{"id" => id}) do
|
|
organization_id = conn.assigns.current_organization_id
|
|
|
|
case ScopedResource.fetch(Check, id, organization_id) do
|
|
{:ok, check} ->
|
|
case Monitoring.delete_check(check) do
|
|
{:ok, _check} ->
|
|
json(conn, %{success: true})
|
|
|
|
{:error, %Ecto.Changeset{} = changeset} ->
|
|
conn
|
|
|> put_status(:unprocessable_entity)
|
|
|> json(%{errors: translate_errors(changeset)})
|
|
end
|
|
|
|
{:error, :forbidden} ->
|
|
conn
|
|
|> put_status(:forbidden)
|
|
|> json(%{error: "Access denied to this check"})
|
|
|
|
{:error, :not_found} ->
|
|
conn
|
|
|> put_status(:not_found)
|
|
|> json(%{error: "Check not found"})
|
|
end
|
|
end
|
|
|
|
defp format_check(check) do
|
|
%{
|
|
id: check.id,
|
|
name: check.name,
|
|
check_type: check.check_type,
|
|
description: check.description,
|
|
enabled: check.enabled,
|
|
device_id: check.device_id,
|
|
agent_token_id: check.agent_token_id,
|
|
interval_seconds: check.interval_seconds,
|
|
timeout_ms: check.timeout_ms,
|
|
retry_interval_seconds: check.retry_interval_seconds,
|
|
max_check_attempts: check.max_check_attempts,
|
|
config: check.config,
|
|
current_state: check.current_state,
|
|
current_state_type: check.current_state_type,
|
|
last_check_at: check.last_check_at,
|
|
inserted_at: check.inserted_at
|
|
}
|
|
end
|
|
|
|
defp maybe_add_opt(opts, _key, nil), do: opts
|
|
defp maybe_add_opt(opts, _key, ""), do: opts
|
|
defp maybe_add_opt(opts, key, value), do: Keyword.put(opts, key, value)
|
|
end
|