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.
118 lines
3.3 KiB
Elixir
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
|