feat(agent): toml config loader with defaults and validation
This commit is contained in:
parent
7ec38e0fd6
commit
e4db0beac6
3 changed files with 147 additions and 0 deletions
77
agent/lib/proxmox_agent/config.ex
Normal file
77
agent/lib/proxmox_agent/config.ex
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
defmodule ProxmoxAgent.Config do
|
||||||
|
@moduledoc "Loads and validates the TOML agent config."
|
||||||
|
|
||||||
|
defstruct [
|
||||||
|
:server_url,
|
||||||
|
:token,
|
||||||
|
:host_id,
|
||||||
|
fast_seconds: 30,
|
||||||
|
medium_seconds: 300,
|
||||||
|
slow_seconds: 1800
|
||||||
|
]
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
server_url: String.t(),
|
||||||
|
token: String.t(),
|
||||||
|
host_id: String.t(),
|
||||||
|
fast_seconds: pos_integer(),
|
||||||
|
medium_seconds: pos_integer(),
|
||||||
|
slow_seconds: pos_integer()
|
||||||
|
}
|
||||||
|
|
||||||
|
@required ~w(server_url token)a
|
||||||
|
|
||||||
|
@spec load(Path.t()) ::
|
||||||
|
{:ok, t()}
|
||||||
|
| {:error, {:file_read, term()} | {:parse, term()} | {:missing_key, atom()}}
|
||||||
|
def load(path) do
|
||||||
|
with {:ok, body} <- read_file(path),
|
||||||
|
{:ok, parsed} <- parse_toml(body),
|
||||||
|
:ok <- validate_required(parsed) do
|
||||||
|
{:ok, build(parsed)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp read_file(path) do
|
||||||
|
case File.read(path) do
|
||||||
|
{:ok, body} -> {:ok, body}
|
||||||
|
{:error, reason} -> {:error, {:file_read, reason}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_toml(body) do
|
||||||
|
case Toml.decode(body) do
|
||||||
|
{:ok, map} -> {:ok, map}
|
||||||
|
{:error, reason} -> {:error, {:parse, reason}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_required(map) do
|
||||||
|
Enum.find_value(@required, :ok, fn key ->
|
||||||
|
case Map.get(map, Atom.to_string(key)) do
|
||||||
|
v when is_binary(v) and v != "" -> nil
|
||||||
|
_ -> {:error, {:missing_key, key}}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build(map) do
|
||||||
|
intervals = Map.get(map, "intervals", %{})
|
||||||
|
|
||||||
|
%__MODULE__{
|
||||||
|
server_url: map["server_url"],
|
||||||
|
token: map["token"],
|
||||||
|
host_id: map["host_id"] || hostname(),
|
||||||
|
fast_seconds: Map.get(intervals, "fast_seconds", 30),
|
||||||
|
medium_seconds: Map.get(intervals, "medium_seconds", 300),
|
||||||
|
slow_seconds: Map.get(intervals, "slow_seconds", 1800)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp hostname do
|
||||||
|
case :inet.gethostname() do
|
||||||
|
{:ok, name} -> List.to_string(name)
|
||||||
|
_ -> "unknown-host"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
8
agent/test/fixtures/agent.toml
vendored
Normal file
8
agent/test/fixtures/agent.toml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
server_url = "wss://monitor.example.com/socket/websocket"
|
||||||
|
token = "test_token_123"
|
||||||
|
host_id = "pve-test-01"
|
||||||
|
|
||||||
|
[intervals]
|
||||||
|
fast_seconds = 15
|
||||||
|
medium_seconds = 120
|
||||||
|
slow_seconds = 600
|
||||||
62
agent/test/proxmox_agent/config_test.exs
Normal file
62
agent/test/proxmox_agent/config_test.exs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
defmodule ProxmoxAgent.ConfigTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias ProxmoxAgent.Config
|
||||||
|
|
||||||
|
@fixture Path.expand("../fixtures/agent.toml", __DIR__)
|
||||||
|
|
||||||
|
describe "load/1" do
|
||||||
|
test "parses required fields" do
|
||||||
|
assert {:ok, cfg} = Config.load(@fixture)
|
||||||
|
assert cfg.server_url == "wss://monitor.example.com/socket/websocket"
|
||||||
|
assert cfg.token == "test_token_123"
|
||||||
|
assert cfg.host_id == "pve-test-01"
|
||||||
|
assert cfg.fast_seconds == 15
|
||||||
|
assert cfg.medium_seconds == 120
|
||||||
|
assert cfg.slow_seconds == 600
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns error for missing file" do
|
||||||
|
assert {:error, {:file_read, _}} = Config.load("/does/not/exist.toml")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "defaults host_id to system hostname when absent" do
|
||||||
|
tmp = Path.join(System.tmp_dir!(), "agent_nohost.toml")
|
||||||
|
|
||||||
|
File.write!(tmp, """
|
||||||
|
server_url = "wss://x/socket/websocket"
|
||||||
|
token = "t"
|
||||||
|
""")
|
||||||
|
|
||||||
|
on_exit(fn -> File.rm(tmp) end)
|
||||||
|
|
||||||
|
assert {:ok, cfg} = Config.load(tmp)
|
||||||
|
assert is_binary(cfg.host_id)
|
||||||
|
assert cfg.host_id != ""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "applies default intervals when [intervals] is absent" do
|
||||||
|
tmp = Path.join(System.tmp_dir!(), "agent_nointervals.toml")
|
||||||
|
|
||||||
|
File.write!(tmp, """
|
||||||
|
server_url = "wss://x/socket/websocket"
|
||||||
|
token = "t"
|
||||||
|
host_id = "h"
|
||||||
|
""")
|
||||||
|
|
||||||
|
on_exit(fn -> File.rm(tmp) end)
|
||||||
|
|
||||||
|
assert {:ok, cfg} = Config.load(tmp)
|
||||||
|
assert cfg.fast_seconds == 30
|
||||||
|
assert cfg.medium_seconds == 300
|
||||||
|
assert cfg.slow_seconds == 1800
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns error when required keys missing" do
|
||||||
|
tmp = Path.join(System.tmp_dir!(), "agent_bad.toml")
|
||||||
|
File.write!(tmp, "token = \"t\"\n")
|
||||||
|
on_exit(fn -> File.rm(tmp) end)
|
||||||
|
assert {:error, {:missing_key, :server_url}} = Config.load(tmp)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Add a link
Reference in a new issue