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 }