lib/nomad_crd/backend/nomad_backend.ex

defmodule NomadCrd.NomadBackend do
  alias NomadClient.Api
  alias NomadClient.Model

  @wait_timeout 30_000
  @watch_sleep 1_000

  @spec create_job(%Model.Job{}) :: {:ok, %Model.Job{}}
  def create_job(%Model.Job{} = job) do
    payload = %Model.RegisterJobRequest{Job: job}
    conn = conn()

    conn
    |> Api.Jobs.register_job(payload)
    |> handle_response()
  end

  def update_job(job_id, job_update) do
    conn = conn()

    job_update = Map.put(job_update, :ID, job_id)
    payload = %Model.RegisterJobRequest{Job: job_update}

    Api.Jobs.update_job(conn, job_id, payload)
    |> handle_response()
  end

  def delete_job(job_id) do
    conn = conn()

    Api.Jobs.stop_job(conn, job_id)
    |> handle_response()
  end

  def get_jobs do
    conn = conn()

    {:ok, job_stubs} = Api.Jobs.get_jobs(conn)

    jobs = Enum.map(job_stubs, &complete_job_stub/1)

    {:ok, jobs}
  end

  def get_job(job_id) do
    conn = conn()

    Api.Jobs.get_job(conn, job_id)
  end

  defp complete_job_stub(%Model.JobListStub{ID: id}) do
    conn = conn()

    {:ok, job} = Api.Jobs.get_job(conn, id)
    job
  end

  defp handle_response({:ok, %{__struct__: struct, EvalID: eval_id}})
       when struct in [Model.JobRegisterResponse, Model.JobDeregisterResponse] do
    conn = conn()

    {:ok, %Model.Evaluation{} = eval} = wait_for_deployment_id(eval_id, 5_000)

    eval
    |> Map.fetch!(:DeploymentID)
    |> wait_for_ready_status(@wait_timeout)
    |> case do
      {:ok, deployment} ->
        job_id = Map.get(deployment, :JobID)
        Api.Jobs.get_job(conn, job_id)

      {:error, :timeout} ->
        {:error, :timeout, eval}
    end
  end

  defp conn do
    # TODO: Must be configurable...
    NomadClient.Connection.new()
  end

  defp wait_for_deployment_id(eval_id, timeout) when is_binary(eval_id) do
    conn = conn()

    wait_for(
      fn -> Api.Evaluations.get_evaluation(conn, eval_id) end,
      fn {:ok, %{DeploymentID: deployment_id}} -> is_binary(deployment_id) end,
      timeout
    )
  end

  defp wait_for_ready_status(deployment_id, timeout) when is_binary(deployment_id) do
    conn = conn()

    wait_for(
      fn -> Api.Deployments.get_deployment(conn, deployment_id) end,
      fn {:ok, %{Status: status}} -> status == "successful" end,
      timeout
    )
  end

  defp wait_for(prepare_fn, condition_fn, timeout) do
    task =
      fn -> wait_for(prepare_fn, condition_fn) end
      |> Task.async()

    case Task.yield(task, timeout) || Task.shutdown(task) do
      {:ok, result} ->
        result

      nil ->
        require Logger
        Logger.warn("Failed to get a result in #{timeout}ms")
        {:error, :timeout}
    end
  end

  defp wait_for(prepare_fn, condition_fn) do
    result = prepare_fn.()

    if condition_fn.(result) !== true do
      :timer.sleep(@watch_sleep)
      wait_for(prepare_fn, condition_fn)
    else
      result
    end
  end
end