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>
181 lines
4.7 KiB
Go
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
|
|
}
|