Files
mcns/internal/grpcserver/zones.go
Kyle Isom f8f3a9868a Apply review fixes: validation, idempotency, SOA dedup, startup cleanup
- Migration v2: INSERT → INSERT OR IGNORE for idempotency
- Config: validate server.tls_cert and server.tls_key are non-empty
- gRPC: add input validation matching REST handlers
- gRPC: add logger to zone/record services, log timestamp parse errors
- REST+gRPC: extract SOA defaults into shared db.ApplySOADefaults()
- DNS: simplify SOA query condition (remove dead code from precedence bug)
- Startup: consolidate shutdown into shutdownAll(), clean up gRPC listener
  on error path, shut down sibling servers when one fails

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:17:15 -07:00

135 lines
3.9 KiB
Go

package grpcserver
import (
"context"
"errors"
"log/slog"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
pb "git.wntrmute.dev/kyle/mcns/gen/mcns/v1"
"git.wntrmute.dev/kyle/mcns/internal/db"
)
type zoneService struct {
pb.UnimplementedZoneServiceServer
db *db.DB
logger *slog.Logger
}
func (s *zoneService) ListZones(_ context.Context, _ *pb.ListZonesRequest) (*pb.ListZonesResponse, error) {
zones, err := s.db.ListZones()
if err != nil {
return nil, status.Error(codes.Internal, "failed to list zones")
}
resp := &pb.ListZonesResponse{}
for _, z := range zones {
resp.Zones = append(resp.Zones, s.zoneToProto(z))
}
return resp, nil
}
func (s *zoneService) GetZone(_ context.Context, req *pb.GetZoneRequest) (*pb.Zone, error) {
if req.Name == "" {
return nil, status.Error(codes.InvalidArgument, "name is required")
}
zone, err := s.db.GetZone(req.Name)
if errors.Is(err, db.ErrNotFound) {
return nil, status.Error(codes.NotFound, "zone not found")
}
if err != nil {
return nil, status.Error(codes.Internal, "failed to get zone")
}
return s.zoneToProto(*zone), nil
}
func (s *zoneService) CreateZone(_ context.Context, req *pb.CreateZoneRequest) (*pb.Zone, error) {
if req.Name == "" {
return nil, status.Error(codes.InvalidArgument, "name is required")
}
if req.PrimaryNs == "" {
return nil, status.Error(codes.InvalidArgument, "primary_ns is required")
}
if req.AdminEmail == "" {
return nil, status.Error(codes.InvalidArgument, "admin_email is required")
}
refresh, retry, expire, minTTL := db.ApplySOADefaults(int(req.Refresh), int(req.Retry), int(req.Expire), int(req.MinimumTtl))
zone, err := s.db.CreateZone(req.Name, req.PrimaryNs, req.AdminEmail, refresh, retry, expire, minTTL)
if errors.Is(err, db.ErrConflict) {
return nil, status.Error(codes.AlreadyExists, err.Error())
}
if err != nil {
return nil, status.Error(codes.Internal, "failed to create zone")
}
return s.zoneToProto(*zone), nil
}
func (s *zoneService) UpdateZone(_ context.Context, req *pb.UpdateZoneRequest) (*pb.Zone, error) {
if req.Name == "" {
return nil, status.Error(codes.InvalidArgument, "name is required")
}
if req.PrimaryNs == "" {
return nil, status.Error(codes.InvalidArgument, "primary_ns is required")
}
if req.AdminEmail == "" {
return nil, status.Error(codes.InvalidArgument, "admin_email is required")
}
refresh, retry, expire, minTTL := db.ApplySOADefaults(int(req.Refresh), int(req.Retry), int(req.Expire), int(req.MinimumTtl))
zone, err := s.db.UpdateZone(req.Name, req.PrimaryNs, req.AdminEmail, refresh, retry, expire, minTTL)
if errors.Is(err, db.ErrNotFound) {
return nil, status.Error(codes.NotFound, "zone not found")
}
if err != nil {
return nil, status.Error(codes.Internal, "failed to update zone")
}
return s.zoneToProto(*zone), nil
}
func (s *zoneService) DeleteZone(_ context.Context, req *pb.DeleteZoneRequest) (*pb.DeleteZoneResponse, error) {
if req.Name == "" {
return nil, status.Error(codes.InvalidArgument, "name is required")
}
err := s.db.DeleteZone(req.Name)
if errors.Is(err, db.ErrNotFound) {
return nil, status.Error(codes.NotFound, "zone not found")
}
if err != nil {
return nil, status.Error(codes.Internal, "failed to delete zone")
}
return &pb.DeleteZoneResponse{}, nil
}
func (s *zoneService) zoneToProto(z db.Zone) *pb.Zone {
return &pb.Zone{
Id: z.ID,
Name: z.Name,
PrimaryNs: z.PrimaryNS,
AdminEmail: z.AdminEmail,
Refresh: int32(z.Refresh),
Retry: int32(z.Retry),
Expire: int32(z.Expire),
MinimumTtl: int32(z.MinimumTTL),
Serial: z.Serial,
CreatedAt: s.parseTimestamp(z.CreatedAt),
UpdatedAt: s.parseTimestamp(z.UpdatedAt),
}
}
func (s *zoneService) parseTimestamp(v string) *timestamppb.Timestamp {
t, err := parseTime(v)
if err != nil {
s.logger.Warn("failed to parse zone timestamp", "value", v, "error", err)
return nil
}
return timestamppb.New(t)
}