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>
276 lines
8.2 KiB
Go
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)
|
|
}
|