elektrine/apps/elektrine_dns/lib/mix/tasks/elektrine.dns.edge_site.ex
maxfield e594946915 fix: address review feedback for edge config bundle
Treat If-None-Match as authoritative for 304 so a stale etag cannot hide
behind a still-valid since; drive content_updated_at from origin, site
pool, and rule revisions. Rate-limit config and heartbeat edge routes.
2026-08-02 01:21:25 -04:00

118 lines
3.3 KiB
Elixir

defmodule Mix.Tasks.Elektrine.Dns.EdgeSite do
@moduledoc """
Instance-admin helper for edge site registry (per-site config tokens).
mix elektrine.dns.edge_site list
mix elektrine.dns.edge_site create --name edge-b --ipv4 203.0.113.10
mix elektrine.dns.edge_site rotate --id <site-id>
mix elektrine.dns.edge_site revoke --id <site-id>
Tokens (`ess_...`) and HMAC secrets are printed **once** on create/rotate.
Store them on the remote edge node; they are never recoverable from the DB.
"""
use Mix.Task
@shortdoc "Manage DNS edge sites (mint/list/rotate/revoke)"
@impl Mix.Task
def run(args) do
Mix.Task.run("app.start")
{opts, rest, _invalid} =
OptionParser.parse(args,
strict: [name: :string, id: :string, ipv4: :string, ipv6: :string, roles: :string]
)
case rest do
["list"] -> list_sites()
["create"] -> create_site(opts)
["rotate"] -> rotate_site(opts)
["revoke"] -> revoke_site(opts)
_ -> usage()
end
end
defp list_sites do
Elektrine.DNS.EdgeSites.list_sites()
|> Enum.each(fn site ->
Mix.shell().info(
"#{site.id} name=#{site.name} status=#{site.status} " <>
"ipv4=#{Enum.join(site.ipv4 || [], ",")} prefix=#{site.token_prefix}"
)
end)
end
defp create_site(opts) do
name = Keyword.get(opts, :name) || Mix.raise("--name is required")
attrs = %{
"name" => name,
"ipv4" => split_csv(Keyword.get(opts, :ipv4)),
"ipv6" => split_csv(Keyword.get(opts, :ipv6)),
"roles" => split_csv(Keyword.get(opts, :roles)) |> default_roles()
}
case Elektrine.DNS.EdgeSites.create_site(attrs) do
{:ok, site} ->
Mix.shell().info("created site id=#{site.id} name=#{site.name}")
Mix.shell().info("token (store once): #{site.token}")
Mix.shell().info("hmac_secret (store once): #{site.hmac_secret_plain}")
{:error, changeset} ->
Mix.raise("create failed: #{inspect(changeset.errors)}")
end
end
defp rotate_site(opts) do
site = fetch_site!(opts)
case Elektrine.DNS.EdgeSites.rotate_token(site) do
{:ok, site} ->
Mix.shell().info("rotated site id=#{site.id}")
Mix.shell().info("token (store once): #{site.token}")
Mix.shell().info("hmac_secret (store once): #{site.hmac_secret_plain}")
{:error, reason} ->
Mix.raise("rotate failed: #{inspect(reason)}")
end
end
defp revoke_site(opts) do
site = fetch_site!(opts)
case Elektrine.DNS.EdgeSites.revoke_site(site) do
{:ok, site} ->
Mix.shell().info("revoked site id=#{site.id}")
{:error, reason} ->
Mix.raise("revoke failed: #{inspect(reason)}")
end
end
defp fetch_site!(opts) do
id = Keyword.get(opts, :id) || Mix.raise("--id is required")
case Elektrine.DNS.EdgeSites.get_site(id) do
nil -> Mix.raise("site not found: #{id}")
site -> site
end
end
defp split_csv(nil), do: []
defp split_csv(""), do: []
defp split_csv(value) when is_binary(value) do
value
|> String.split(",", trim: true)
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
end
defp default_roles([]), do: ["proxy"]
defp default_roles(roles), do: roles
defp usage do
Mix.shell().info(@moduledoc)
end
end