Files
gsc-ops-api/internal/client/powerdns.go
Claude (gsc-ops-api init) 3847eb2036 Initial import — snapshot from admin host /srv/gosec/gsc-ops-api
This repo had no version control prior to this commit. The import is a
straight snapshot of the working tree at 2026-05-03; the deployed
binary on fihelvop01 was being rebuilt from this source via `make
build` + scp into place, with no upstream review path.

The snapshot already includes one in-flight fix made on 2026-05-03 to
internal/service/persona.go:GetSelfModel — the handler queried
`source` and `strength` columns plus an `is_active = true` filter on
persona.persona_commitments, none of which exist on that table (its
shape is session-bound commitments with `status`, `commitment_meta`,
etc.). The query returned a 500 every time SynapseHub bootstrapped a
persona's self-model, dropping the IdentityConstraints / Commitments /
ConscienceStandards layer from the assembled prompt. The patched
query reads existing columns only (commitment_text, commitment_type),
filters on `status='active'`, and synthesises Source="learned" /
Strength=1.0 to keep the SelfModel response shape stable for callers.

Verified live: `GET /api/v1/personas/70f7cfd9-.../self-model` now
returns 200 with `{identityConstraints:[],commitments:[],
conscienceStandards:[]}` instead of 500.

Future changes go through PRs against this repo — no more bin-only
deploys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:06:02 +02:00

181 lines
4.7 KiB
Go

package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/rs/zerolog"
"github.com/gosec/gsc-ops-api/internal/config"
)
// PowerDNSClient is an HTTP client for the PowerDNS API
type PowerDNSClient struct {
baseURL string
apiKey string
serverID string
client *http.Client
logger zerolog.Logger
}
// NewPowerDNSClient creates a new PowerDNS client
func NewPowerDNSClient(cfg config.PowerDNSConfig, logger zerolog.Logger) *PowerDNSClient {
return &PowerDNSClient{
baseURL: cfg.BaseURL,
apiKey: cfg.APIKey,
serverID: cfg.ServerID,
client: &http.Client{Timeout: 30 * time.Second},
logger: logger.With().Str("component", "powerdns").Logger(),
}
}
// RRSet represents a PowerDNS resource record set
type RRSet struct {
Name string `json:"name"`
Type string `json:"type"`
TTL int `json:"ttl"`
ChangeType string `json:"changetype,omitempty"`
Records []Record `json:"records"`
Comments []Comment `json:"comments,omitempty"`
}
// Record represents a single DNS record
type Record struct {
Content string `json:"content"`
Disabled bool `json:"disabled"`
}
// Comment represents a comment on an RRSet
type Comment struct {
Content string `json:"content"`
Account string `json:"account"`
ModifiedAt int64 `json:"modified_at"`
}
// Zone represents a PowerDNS zone
type Zone struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
DNSSec bool `json:"dnssec"`
Serial int64 `json:"serial"`
NotifiedSerial int64 `json:"notified_serial"`
SOAEdit string `json:"soa_edit,omitempty"`
SOAEditAPI string `json:"soa_edit_api,omitempty"`
RRSets []RRSet `json:"rrsets,omitempty"`
Nameservers []string `json:"nameservers,omitempty"`
Masters []string `json:"masters,omitempty"`
}
// ZoneCreate is the request body for creating a zone
type ZoneCreate struct {
Name string `json:"name"`
Kind string `json:"kind"`
Nameservers []string `json:"nameservers"`
Masters []string `json:"masters,omitempty"`
}
func (c *PowerDNSClient) do(method, path string, body interface{}, result interface{}) error {
url := fmt.Sprintf("%s/api/v1/servers/%s%s", c.baseURL, c.serverID, path)
var reqBody io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
reqBody = bytes.NewReader(data)
}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("X-API-Key", c.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode >= 400 {
return fmt.Errorf("PowerDNS error %d: %s", resp.StatusCode, string(respBody))
}
if result != nil && len(respBody) > 0 {
if err := json.Unmarshal(respBody, result); err != nil {
return fmt.Errorf("failed to parse response: %w", err)
}
}
return nil
}
// ListZones lists all zones
func (c *PowerDNSClient) ListZones() ([]Zone, error) {
var zones []Zone
err := c.do("GET", "/zones", nil, &zones)
return zones, err
}
// GetZone gets a zone by ID with RRSets
func (c *PowerDNSClient) GetZone(zoneID string) (*Zone, error) {
var zone Zone
err := c.do("GET", "/zones/"+zoneID, nil, &zone)
if err != nil {
return nil, err
}
return &zone, nil
}
// CreateZone creates a new zone
func (c *PowerDNSClient) CreateZone(zone *ZoneCreate) (*Zone, error) {
var result Zone
err := c.do("POST", "/zones", zone, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// UpdateZone updates zone metadata (PATCH to /zones/:id is for metadata only)
func (c *PowerDNSClient) UpdateZone(zoneID string, data map[string]interface{}) error {
return c.do("PUT", "/zones/"+zoneID, data, nil)
}
// DeleteZone deletes a zone
func (c *PowerDNSClient) DeleteZone(zoneID string) error {
return c.do("DELETE", "/zones/"+zoneID, nil, nil)
}
// NotifyZone sends NOTIFY to slaves
func (c *PowerDNSClient) NotifyZone(zoneID string) error {
return c.do("PUT", "/zones/"+zoneID+"/notify", nil, nil)
}
// PatchRRSets patches record sets in a zone (create, update, or delete)
func (c *PowerDNSClient) PatchRRSets(zoneID string, rrsets []RRSet) error {
body := map[string]interface{}{
"rrsets": rrsets,
}
return c.do("PATCH", "/zones/"+zoneID, body, nil)
}
// Health checks PowerDNS connectivity
func (c *PowerDNSClient) Health() error {
_, err := c.ListZones()
return err
}