lib/chaperon/api/http.ex

defmodule Chaperon.API.HTTP do
  @moduledoc """
  HTTP API handler used for remote control of chaperon cluster.
  Allows scheduling new load tests, aborting currently running or scheduled ones
  as well as listing all currently running and scheduled load tests.
  """

  require Logger
  require Poison

  defmodule HealthCheckPlug do
    @moduledoc false

    import Plug.Conn
    @behaviour Plug

    @intercepted_routes [
      # root page (GET /)
      [],
      ["healthcheck"]
    ]

    def init(opts), do: opts

    def call(conn = %{method: "GET", path_info: path_info}, _opts)
        when path_info in @intercepted_routes do
      conn
      |> send_resp(200, "Chaperon @ #{Chaperon.version()}")
      |> halt()
    end

    def call(conn, _opts), do: conn
  end

  use Plug.Router
  import Plug.Conn
  import Chaperon.Util, only: [symbolize_keys: 1]

  if Mix.env() == :dev do
    use Plug.Debugger, otp_app: :cutlass
  end

  plug(Chaperon.API.HTTP.HealthCheckPlug)
  plug(:self_logger)
  plug(Plug.RequestId)
  plug(BasicAuth, use_config: {:chaperon, Chaperon.API.HTTP})
  plug(Plug.Parsers, parsers: [:json], json_decoder: Poison)
  plug(:match)
  plug(:dispatch)

  def start_link() do
    port = api_port()
    Logger.info("Starting Chaperon.API.HTTP on port #{port}")

    case api_ip() do
      nil -> Plug.Adapters.Cowboy.http(__MODULE__, [acceptors: 20], port: port)
      ip -> Plug.Adapters.Cowboy.http(__MODULE__, [acceptors: 20], port: port, ip: ip)
    end
  end

  def enabled?() do
    api_port() != nil
  end

  def api_ip() do
    case System.get_env("CHAPERON_IP") do
      nil ->
        Application.get_env(:chaperon, __MODULE__)[:ip]

      ip ->
        String.to_charlist(ip) |> :inet.parse_address() |> elem(1)
    end
  end

  def api_port() do
    case System.get_env("CHAPERON_PORT") do
      nil -> Application.get_env(:chaperon, __MODULE__)[:port]
      port -> port |> String.to_integer()
    end
  end

  get "/load_tests" do
    conn
    |> send_json_resp(200, %{
      running: Chaperon.Master.running_load_tests(),
      scheduled: Chaperon.Master.scheduled_load_tests()
    })
  end

  get "/version" do
    conn
    |> send_resp(200, Chaperon.version())
  end

  get "/*_" do
    conn
    |> send_resp(404, "")
  end

  post "/load_tests" do
    load_tests =
      for cfg <- conn.params["load_tests"] || [] do
        cfg
        |> Map.take(["test", "options"])
        |> symbolize_keys()
        |> parse_options()
      end
      |> List.flatten()

    case Chaperon.Master.schedule_load_tests(load_tests) do
      {:error, reason} ->
        conn
        |> send_json_resp(400, %{error: reason |> inspect})

      {:ok, ids} ->
        conn
        |> send_json_resp(200, %{scheduled: ids})
    end
  end

  delete "/load_tests" do
    case Chaperon.Master.cancel_all() do
      {:error, reason} ->
        conn
        |> send_json_resp(400, %{
          error: inspect(reason)
        })

      :ok ->
        conn
        |> send_resp(202, "")
    end
  end

  delete "/load_tests/scheduled" do
    case Chaperon.Master.cancel_scheduled() do
      {:error, reason} ->
        conn
        |> send_json_resp(400, %{
          error: inspect(reason)
        })

      :ok ->
        conn
        |> send_resp(202, "")
    end
  end

  delete "/load_tests/:id" do
    case Chaperon.Master.cancel_running_or_scheduled(conn.params["id"]) do
      {:error, reason} ->
        conn
        |> send_json_resp(400, %{
          error: inspect(reason)
        })

      :ok ->
        conn
        |> send_resp(202, "")
    end
  end

  post "/*_" do
    conn
    |> send_resp(404, "")
  end

  defp self_logger(conn, opts) do
    if conn.request_path == "/healthcheck" do
      conn
    else
      Plug.Logger.call(conn, Plug.Logger.init(opts))
    end
  end

  defp send_json_resp(conn, status_code, data) do
    conn
    |> send_resp(status_code, Poison.encode!(data))
  end

  def parse_options(args) do
    parser = Application.get_env(:chaperon, __MODULE__)[:option_parser]
    Logger.info("Chaperon.API.HTTP | Using option parser: #{inspect(parser)}")

    case parser.parse_options(args) do
      {:ok, lt_configs} ->
        for {lt, options} <- lt_configs do
          %{test: lt, options: options}
        end

      {:ok, lt, options} ->
        %{test: lt, options: options}

      {:error, reason} ->
        Logger.error("Error parsing options using parser #{inspect(parser)}: #{inspect(reason)}")
        raise ArgumentError, message: inspect(reason)
    end
  end
end