lib/k8s/conn/auth/azure.ex

defmodule K8s.Conn.Auth.Azure do
  @moduledoc """
  `auth-provider` for azure
  """
  alias K8s.Conn.RequestOptions

  require Logger
  @behaviour K8s.Conn.Auth

  defstruct [:token]

  @type t :: %__MODULE__{
          token: String.t()
        }

  @impl true
  @spec create(map, String.t()) :: {:ok, t} | :skip
  def create(
        %{
          "auth-provider" => %{
            "config" => %{
              "access-token" => token,
              "tenant-id" => tenant,
              "expires-on" => expires_on,
              "refresh-token" => refresh_token,
              "client-id" => client_id,
              "apiserver-id" => apiserver_id
            },
            "name" => "azure"
          }
        },
        _
      ) do
    if DateTime.diff(DateTime.utc_now(), parse_expires(expires_on)) >= 0 do
      Logger.info(
        "Azure token expired, using refresh token get new access, this will stop working when refresh token expires"
      )

      {:ok, %__MODULE__{token: refresh_token(tenant, refresh_token, client_id, apiserver_id)}}
    else
      {:ok, %__MODULE__{token: token}}
    end
  end

  def create(_, _), do: :skip

  @spec parse_expires(String.t()) :: DateTime.t()
  defp parse_expires(expires_on) do
    case Integer.parse(expires_on) do
      {expires_on, _} -> DateTime.from_unix!(expires_on)
      :error -> DateTime.from_iso8601(expires_on)
    end
  end

  @spec refresh_token(String.t(), String.t(), String.t(), String.t()) :: String.t()
  def refresh_token(tenant, refresh_token, client_id, _apiserver_id) do
    payload =
      URI.encode_query(%{
        "client_id" => client_id,
        "grant_type" => "refresh_token",
        "refresh_token" => refresh_token
      })

    {:ok, res} =
      K8s.Client.MintHTTPProvider.request(
        :post,
        URI.new!("https://login.microsoftonline.com/#{tenant}/oauth2/v2.0/token"),
        payload,
        [
          {
            "Content-Type",
            "application/x-www-form-urlencoded"
          }
        ],
        ssl: []
      )

    Map.get(res, "access_token")
  end

  defimpl RequestOptions, for: __MODULE__ do
    @spec generate(K8s.Conn.Auth.Azure.t()) :: RequestOptions.generate_t()
    def generate(%K8s.Conn.Auth.Azure{token: token}) do
      {:ok,
       %RequestOptions{
         headers: [{:Authorization, "Bearer #{token}"}],
         ssl_options: []
       }}
    end
  end
end