core/config/core.ex

# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved.

use Croma

defmodule AntikytheraCore.Config.Core do
  alias Croma.Result, as: R
  alias Antikythera.SecondsSinceEpoch
  alias Antikythera.Crypto.Aes
  alias AntikytheraCore.Path, as: CorePath
  alias AntikytheraCore.Ets.ConfigCache
  alias AntikytheraCore.Config.EncryptionKey
  alias AntikytheraCore.Alert.Manager, as: CoreAlertManager
  require AntikytheraCore.Logger, as: L

  defunp read(last_changed_at :: v[SecondsSinceEpoch.t()]) ::
           {map, SecondsSinceEpoch.t()} | :not_modified do
    path = CorePath.core_config_file_path()

    case File.stat(path, time: :posix) do
      {:ok, %File.Stat{mtime: mtime}} ->
        if mtime > last_changed_at do
          {:ok, value} = File.read!(path) |> decrypt_and_eval()
          {value, mtime}
        else
          :not_modified
        end

      {:error, :enoent} ->
        # config file does not exist (probably during bootstrapping of antikythera); create one with an empty map and retry
        write(%{})
        read(0)
    end
  end

  defunp decrypt_and_eval(encrypted :: v[binary]) :: R.t(map) do
    Aes.ctr128_decrypt(encrypted, EncryptionKey.get())
    |> R.bind(&eval/1)
  end

  defunp eval(b :: v[binary]) :: R.t(map) do
    R.try(fn ->
      {value, _bindings} = Code.eval_string(b)
      value
    end)
  end

  defun load(last_changed_at :: v[SecondsSinceEpoch.t()] \\ 0) :: SecondsSinceEpoch.t() do
    case read(last_changed_at) do
      :not_modified ->
        last_changed_at

      {new_config, changed_at} ->
        L.info("found change in core config")
        ConfigCache.Core.write(new_config)
        alert_config = Map.get(new_config, :alerts, %{})
        CoreAlertManager.update_handler_installations(:antikythera, alert_config)
        changed_at
    end
  end

  @doc """
  Writes a map to antikythera's config file.

  Currently this function is intended to be used within remote_console.
  """
  defun write(config :: v[map]) :: :ok do
    path = CorePath.core_config_file_path()
    content = Aes.ctr128_encrypt(inspect(config), EncryptionKey.get())
    CorePath.atomic_write!(path, content)
  end

  defun dump_from_env_to_file() :: :ok do
    env_var = System.get_env("ANTIKYTHERA_CONFIG") || ""
    {content, _bindings} = Code.eval_string(env_var)
    write(content || %{})
  end
end