suporte a dynamic dns
This commit is contained in:
173
internal/appdb/mysql.go
Normal file
173
internal/appdb/mysql.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Package appdb stores application-owned state outside PowerDNS.
|
||||
package appdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
// ErrNotFound indicates that a requested database record does not exist.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// DynamicRecord describes an RRset with dynamic DNS updates enabled.
|
||||
type DynamicRecord struct {
|
||||
ZoneID string
|
||||
Name string
|
||||
Type string
|
||||
TTL uint32
|
||||
TokenHash string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// MySQLStore persists application state in MySQL.
|
||||
type MySQLStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewMySQLStore opens a MySQL-backed application store.
|
||||
func NewMySQLStore(dsn string) (*MySQLStore, error) {
|
||||
db, err := sql.Open("mysql", strings.TrimSpace(dsn))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open mysql database: %w", err)
|
||||
}
|
||||
return &MySQLStore{db: db}, nil
|
||||
}
|
||||
|
||||
// Close releases the underlying database connection pool.
|
||||
func (s *MySQLStore) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
// Ping verifies that MySQL is reachable.
|
||||
func (s *MySQLStore) Ping(ctx context.Context) error {
|
||||
if err := s.db.PingContext(ctx); err != nil {
|
||||
return fmt.Errorf("ping mysql database: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureSchema creates required application tables when they do not exist.
|
||||
func (s *MySQLStore) EnsureSchema(ctx context.Context) error {
|
||||
const query = `
|
||||
CREATE TABLE IF NOT EXISTS dynamic_records (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
zone_id VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(10) NOT NULL,
|
||||
ttl INT UNSIGNED NOT NULL,
|
||||
token_hash CHAR(64) NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uniq_dynamic_record (zone_id, name, type),
|
||||
KEY idx_dynamic_name_token (name, token_hash)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
|
||||
if _, err := s.db.ExecContext(ctx, query); err != nil {
|
||||
return fmt.Errorf("ensure dynamic_records schema: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListDynamicRecords returns enabled dynamic DNS records for a zone.
|
||||
func (s *MySQLStore) ListDynamicRecords(ctx context.Context, zoneID string) ([]DynamicRecord, error) {
|
||||
const query = `
|
||||
SELECT zone_id, name, type, ttl, token_hash, enabled
|
||||
FROM dynamic_records
|
||||
WHERE zone_id = ? AND enabled = TRUE`
|
||||
rows, err := s.db.QueryContext(ctx, query, zoneID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list dynamic records: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
records := make([]DynamicRecord, 0)
|
||||
for rows.Next() {
|
||||
record, err := scanDynamicRecord(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate dynamic records: %w", err)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// FindDynamicRecordByNameAndTokenHash returns the enabled record matching name and token hash.
|
||||
func (s *MySQLStore) FindDynamicRecordByNameAndTokenHash(ctx context.Context, name, tokenHash string) (DynamicRecord, error) {
|
||||
const query = `
|
||||
SELECT zone_id, name, type, ttl, token_hash, enabled
|
||||
FROM dynamic_records
|
||||
WHERE name = ? AND token_hash = ? AND enabled = TRUE`
|
||||
row := s.db.QueryRowContext(ctx, query, name, tokenHash)
|
||||
record, err := scanDynamicRecord(row)
|
||||
if err != nil {
|
||||
return DynamicRecord{}, err
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// GetDynamicRecord returns an enabled dynamic DNS record by identity.
|
||||
func (s *MySQLStore) GetDynamicRecord(ctx context.Context, zoneID, name, recordType string) (DynamicRecord, error) {
|
||||
const query = `
|
||||
SELECT zone_id, name, type, ttl, token_hash, enabled
|
||||
FROM dynamic_records
|
||||
WHERE zone_id = ? AND name = ? AND type = ? AND enabled = TRUE`
|
||||
row := s.db.QueryRowContext(ctx, query, zoneID, name, recordType)
|
||||
record, err := scanDynamicRecord(row)
|
||||
if err != nil {
|
||||
return DynamicRecord{}, err
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// UpsertDynamicRecord enables dynamic DNS for a record and stores its token hash.
|
||||
func (s *MySQLStore) UpsertDynamicRecord(ctx context.Context, record DynamicRecord) error {
|
||||
const query = `
|
||||
INSERT INTO dynamic_records (zone_id, name, type, ttl, token_hash, enabled)
|
||||
VALUES (?, ?, ?, ?, ?, TRUE)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
ttl = VALUES(ttl),
|
||||
token_hash = VALUES(token_hash),
|
||||
enabled = TRUE`
|
||||
if _, err := s.db.ExecContext(ctx, query, record.ZoneID, record.Name, record.Type, record.TTL, record.TokenHash); err != nil {
|
||||
return fmt.Errorf("upsert dynamic record: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableDynamicRecord disables dynamic DNS for a record.
|
||||
func (s *MySQLStore) DisableDynamicRecord(ctx context.Context, zoneID, name, recordType string) error {
|
||||
const query = `
|
||||
UPDATE dynamic_records
|
||||
SET enabled = FALSE
|
||||
WHERE zone_id = ? AND name = ? AND type = ?`
|
||||
if _, err := s.db.ExecContext(ctx, query, zoneID, name, recordType); err != nil {
|
||||
return fmt.Errorf("disable dynamic record: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type dynamicScanner interface {
|
||||
Scan(...any) error
|
||||
}
|
||||
|
||||
func scanDynamicRecord(scanner dynamicScanner) (DynamicRecord, error) {
|
||||
var record DynamicRecord
|
||||
var ttl uint64
|
||||
if err := scanner.Scan(&record.ZoneID, &record.Name, &record.Type, &ttl, &record.TokenHash, &record.Enabled); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return DynamicRecord{}, ErrNotFound
|
||||
}
|
||||
return DynamicRecord{}, fmt.Errorf("scan dynamic record: %w", err)
|
||||
}
|
||||
record.TTL = uint32(ttl)
|
||||
return record, nil
|
||||
}
|
||||
Reference in New Issue
Block a user