Files
DDI/server/internal/httpapi/h_zones.go
T
sir_melonchenandClaude Opus 5.5 2b04661792 feat: clusters, TSIG keys, zones and records in service and REST API
Implements the M5 endpoints of the contract (ADR 0023): clusters with
member validation, transfer keys and HA credentials, TSIG keys with
audited secret access and rotation, explicit zones and typed records,
the DHCP cluster of networks, the cluster of agents and the default
clusters and HA password file of the generator settings. Revisions are
stored per target with sealed secret files and sealed source secrets
(restored for rollbacks); bundles carry an agent's own target or the
default group. Active leases are adopted as dynamic addresses.

The derivation is called through a temporary adapter until derive
implements the M5 contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:47:37 +02:00

276 lines
8.2 KiB
Go

// SPDX-License-Identifier: LicenseRef-DDI-Proprietary
package httpapi
import (
"net/http"
"git.beugsiud.de/beug.k/DDI/server/internal/service"
)
// Clusters, TSIG keys, zones and records (ADR 0023).
type clusterMemberRequest struct {
AgentID string `json:"agent_id"`
Role string `json:"role"`
Address string `json:"address"`
}
type clusterRequest struct {
Name string `json:"name"`
Role string `json:"role"`
Description string `json:"description"`
Members []clusterMemberRequest `json:"members"`
PrimaryNS string `json:"primary_ns"`
NameServers []service.GeneratorNS `json:"name_servers"`
HAMode string `json:"ha_mode"`
HAPort *int `json:"ha_port"`
Version int `json:"version"`
}
func (req *clusterRequest) input() service.ClusterInput {
in := service.ClusterInput{Name: req.Name, Role: req.Role, Description: req.Description, PrimaryNS: req.PrimaryNS,
NameServers: req.NameServers, HAMode: req.HAMode, HAPort: req.HAPort, Version: req.Version}
for _, m := range req.Members {
in.Members = append(in.Members, service.ClusterMemberInput(m))
}
return in
}
func (a *API) listClusters(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
list, err := a.svc.ListClusters(r.Context(), r.URL.Query().Get("role"))
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, listResponse[service.ClusterDTO]{Items: list})
}
func (a *API) createCluster(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
var req clusterRequest
if err := decode(w, r, &req); err != nil {
return err
}
c, err := a.svc.CreateCluster(r.Context(), rc.Actor, req.input())
if err != nil {
return err
}
return writeJSON(w, http.StatusCreated, c)
}
func (a *API) getCluster(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
c, err := a.svc.GetCluster(r.Context(), r.PathValue("id"))
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, c)
}
func (a *API) updateCluster(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
var req clusterRequest
if err := decode(w, r, &req); err != nil {
return err
}
c, err := a.svc.UpdateCluster(r.Context(), rc.Actor, r.PathValue("id"), req.input())
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, c)
}
func (a *API) deleteCluster(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
if err := a.svc.DeleteCluster(r.Context(), rc.Actor, r.PathValue("id")); err != nil {
return err
}
return noContent(w)
}
type tsigKeyRequest struct {
Name string `json:"name"`
Algorithm string `json:"algorithm"`
Description string `json:"description"`
Version int `json:"version"`
}
func (req *tsigKeyRequest) input() service.TSIGKeyInput { return service.TSIGKeyInput(*req) }
func (a *API) listTSIGKeys(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
list, err := a.svc.ListTSIGKeys(r.Context())
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, listResponse[service.TSIGKeyDTO]{Items: list})
}
func (a *API) createTSIGKey(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
var req tsigKeyRequest
if err := decode(w, r, &req); err != nil {
return err
}
k, err := a.svc.CreateTSIGKey(r.Context(), rc.Actor, req.input())
if err != nil {
return err
}
return writeJSON(w, http.StatusCreated, k)
}
func (a *API) updateTSIGKey(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
var req tsigKeyRequest
if err := decode(w, r, &req); err != nil {
return err
}
k, err := a.svc.UpdateTSIGKey(r.Context(), rc.Actor, r.PathValue("id"), req.input())
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, k)
}
func (a *API) deleteTSIGKey(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
if err := a.svc.DeleteTSIGKey(r.Context(), rc.Actor, r.PathValue("id")); err != nil {
return err
}
return noContent(w)
}
func (a *API) getTSIGKeySecret(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
k, err := a.svc.GetTSIGKeySecret(r.Context(), rc.Actor, r.PathValue("id"))
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, k)
}
func (a *API) rotateTSIGKey(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
k, err := a.svc.RotateTSIGKey(r.Context(), rc.Actor, r.PathValue("id"))
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, k)
}
type zoneRequest struct {
Name string `json:"name"`
Kind string `json:"kind"`
Mode string `json:"mode"`
ClusterID *string `json:"cluster_id"`
DefaultTTL *int `json:"default_ttl"`
AllowUpdate []string `json:"allow_update"`
UpdateKeyIDs []string `json:"update_key_ids"`
Description string `json:"description"`
Version int `json:"version"`
}
func (req *zoneRequest) input() service.ZoneInput { return service.ZoneInput(*req) }
func (a *API) listZones(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
list, err := a.svc.ListZones(r.Context())
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, listResponse[service.ZoneDTO]{Items: list})
}
func (a *API) listDerivedZones(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
list, err := a.svc.DerivedZones(r.Context())
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, listResponse[service.DerivedZoneDTO]{Items: list})
}
func (a *API) createZone(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
var req zoneRequest
if err := decode(w, r, &req); err != nil {
return err
}
z, err := a.svc.CreateZone(r.Context(), rc.Actor, req.input())
if err != nil {
return err
}
return writeJSON(w, http.StatusCreated, z)
}
func (a *API) getZone(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
z, err := a.svc.GetZone(r.Context(), r.PathValue("id"))
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, z)
}
func (a *API) updateZone(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
var req zoneRequest
if err := decode(w, r, &req); err != nil {
return err
}
z, err := a.svc.UpdateZone(r.Context(), rc.Actor, r.PathValue("id"), req.input())
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, z)
}
func (a *API) deleteZone(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
force := r.URL.Query().Get("force") == "true"
if err := a.svc.DeleteZone(r.Context(), rc.Actor, r.PathValue("id"), force); err != nil {
return err
}
return noContent(w)
}
type recordRequest struct {
Name string `json:"name"`
Type string `json:"type"`
TTL *int `json:"ttl"`
Address string `json:"address"`
Target string `json:"target"`
Preference int `json:"preference"`
Priority int `json:"priority"`
Weight int `json:"weight"`
Port int `json:"port"`
Text []string `json:"text"`
Description string `json:"description"`
Version int `json:"version"`
}
func (req *recordRequest) input() service.RecordInput { return service.RecordInput(*req) }
func (a *API) listRecords(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
list, err := a.svc.ListRecords(r.Context(), r.PathValue("id"))
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, listResponse[service.RecordDTO]{Items: list})
}
func (a *API) createRecord(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
var req recordRequest
if err := decode(w, r, &req); err != nil {
return err
}
rec, err := a.svc.CreateRecord(r.Context(), rc.Actor, r.PathValue("id"), req.input())
if err != nil {
return err
}
return writeJSON(w, http.StatusCreated, rec)
}
func (a *API) updateRecord(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
var req recordRequest
if err := decode(w, r, &req); err != nil {
return err
}
rec, err := a.svc.UpdateRecord(r.Context(), rc.Actor, r.PathValue("id"), r.PathValue("recordId"), req.input())
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, rec)
}
func (a *API) deleteRecord(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
if err := a.svc.DeleteRecord(r.Context(), rc.Actor, r.PathValue("id"), r.PathValue("recordId")); err != nil {
return err
}
return noContent(w)
}