lib/bonny_plug/admission_review/request.ex

defmodule BonnyPlug.AdmissionReview.Request do
  @moduledoc """
  Helper functions for admission review request handling. This module is imported when using `WebhookHandler`.
  """

  alias BonnyPlug.AdmissionReview

  @doc """
  Responds by allowing the operation

  ## Examples

      iex> admission_review = %BonnyPlug.AdmissionReview{request: %{}, response: %{}}
      ...> BonnyPlug.AdmissionReview.Request.allow(admission_review)
      %BonnyPlug.AdmissionReview{request: %{}, response: %{"allowed" => true}}
  """
  @spec allow(AdmissionReview.t()) :: AdmissionReview.t()
  def allow(admission_review) do
    put_in(admission_review.response["allowed"], true)
  end

  @doc """
  Responds by denying the operation

  ## Examples

      iex> admission_review = %BonnyPlug.AdmissionReview{request: %{}, response: %{}}
      ...> BonnyPlug.AdmissionReview.Request.deny(admission_review)
      %BonnyPlug.AdmissionReview{request: %{}, response: %{"allowed" => false}}
  """
  @spec deny(AdmissionReview.t()) :: AdmissionReview.t()
  def deny(admission_review) do
    put_in(admission_review.response["allowed"], false)
  end

  @doc """
  Responds by denying the operation, returning response code and message

  ## Examples

      iex> admission_review = %BonnyPlug.AdmissionReview{request: %{}, response: %{}}
      ...> BonnyPlug.AdmissionReview.Request.deny(admission_review, 403, "foo")
      %BonnyPlug.AdmissionReview{request: %{}, response: %{"allowed" => false, "status" => %{"code" => 403, "message" => "foo"}}}

      iex> BonnyPlug.AdmissionReview.Request.deny(%BonnyPlug.AdmissionReview{request: %{}, response: %{}}, "foo")
      %BonnyPlug.AdmissionReview{request: %{}, response: %{"allowed" => false, "status" => %{"code" => 400, "message" => "foo"}}}
  """
  @spec deny(AdmissionReview.t(), integer(), binary()) :: AdmissionReview.t()
  @spec deny(AdmissionReview.t(), binary()) :: AdmissionReview.t()
  def deny(admission_review, code \\ 400, message) do
    admission_review
    |> deny()
    |> put_in([Access.key(:response), "status"], %{"code" => code, "message" => message})
  end

  @doc """
  Adds a warning to the admission review's response.

  ## Examples

      iex> admission_review = %BonnyPlug.AdmissionReview{request: %{}, response: %{}}
      ...> BonnyPlug.AdmissionReview.Request.add_warning(admission_review, "warning")
      %BonnyPlug.AdmissionReview{request: %{}, response: %{"warnings" => ["warning"]}}

      iex> admission_review = %BonnyPlug.AdmissionReview{request: %{}, response: %{"warnings" => ["existing_warning"]}}
      ...> BonnyPlug.AdmissionReview.Request.add_warning(admission_review, "new_warning")
      %BonnyPlug.AdmissionReview{request: %{}, response: %{"warnings" => ["new_warning", "existing_warning"]}}
  """
  @spec add_warning(AdmissionReview.t(), binary()) :: AdmissionReview.t()
  def add_warning(admission_review, warning) do
    update_in(admission_review, [Access.key(:response), Access.key("warnings", [])], &([warning | &1]))
  end

  @doc """
  Verifies that a given field has not been mutated.

  ## Examples

      iex> admission_review = %BonnyPlug.AdmissionReview{request: %{"object" => %{"spec" => %{"immutable" => "value"}}, "oldObject" => %{"spec" => %{"immutable" => "value"}}}, response: %{}}
      ...> BonnyPlug.AdmissionReview.Request.check_immutable(admission_review, ["spec", "immutable"])
      %BonnyPlug.AdmissionReview{request: %{"object" => %{"spec" => %{"immutable" => "value"}}, "oldObject" => %{"spec" => %{"immutable" => "value"}}}, response: %{}}

      iex> admission_review = %BonnyPlug.AdmissionReview{request: %{"object" => %{"spec" => %{"immutable" => "new_value"}}, "oldObject" => %{"spec" => %{"immutable" => "value"}}}, response: %{}}
      ...> BonnyPlug.AdmissionReview.Request.check_immutable(admission_review, ["spec", "immutable"])
      %BonnyPlug.AdmissionReview{request: %{"object" => %{"spec" => %{"immutable" => "new_value"}}, "oldObject" => %{"spec" => %{"immutable" => "value"}}}, response: %{"allowed" => false, "status" => %{"code" => 400, "message" => "The field .spec.immutable is immutable."}}}
  """
  @spec check_immutable(AdmissionReview.t(), list()) :: AdmissionReview.t()
  def check_immutable(admission_review, field) do
    new_value = get_in(admission_review.request, ["object" | field])
    old_value = get_in(admission_review.request, ["oldObject" | field])

    if new_value == old_value, do: admission_review, else: deny(admission_review, "The field .#{Enum.join(field, ".")} is immutable.")
  end

  @doc """
  Checks the given field's value - if defined - against a list of allowed values. If the field is not defined, the
  request is considered valid and no error is returned. Use the CRD to define required fields.

  ## Examples

      iex> admission_review = %BonnyPlug.AdmissionReview{request: %{"object" => %{"metadata" => %{"annotations" => %{"some/annotation" => "bar"}}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}}
      ...> BonnyPlug.AdmissionReview.Request.check_allowed_values(admission_review, ~w(metadata annotations some/annotation), ["foo", "bar"])
      %BonnyPlug.AdmissionReview{request: %{"object" => %{"metadata" => %{"annotations" => %{"some/annotation" => "bar"}}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}}

      iex> admission_review = %BonnyPlug.AdmissionReview{request: %{"object" => %{"metadata" => %{}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}}
      ...> BonnyPlug.AdmissionReview.Request.check_allowed_values(admission_review, ~w(metadata annotations some/annotation), ["foo", "bar"])
      %BonnyPlug.AdmissionReview{request: %{"object" => %{"metadata" => %{}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}}

      iex> admission_review = %BonnyPlug.AdmissionReview{request: %{"object" => %{"metadata" => %{"annotations" => %{"some/annotation" => "other"}}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}}
      ...> BonnyPlug.AdmissionReview.Request.check_allowed_values(admission_review, ~w(metadata annotations some/annotation), ["foo", "bar"])
      %BonnyPlug.AdmissionReview{request: %{"object" => %{"metadata" => %{"annotations" => %{"some/annotation" => "other"}}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{"allowed" => false, "status" => %{"code" => 400, "message" => ~S(The field .metadata.annotations.some/annotation must contain one of the values in ["foo", "bar"] but it's currently set to "other".)}}}
  """
  @spec check_allowed_values(AdmissionReview.t(), list(), list()) :: AdmissionReview.t()
  def check_allowed_values(admission_review, field, allowed_values) do
    value = get_in(admission_review.request, ["object" | field])

    if is_nil(value) or value in allowed_values,
      do: admission_review,
      else: deny(admission_review, "The field .metadata.annotations.some/annotation must contain one of the values in #{inspect(allowed_values)} but it's currently set to #{inspect(value)}.")
  end
end