From f9635578e07fcc511e2bdad7f5cc9399594e5fbe Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 26 Mar 2026 18:37:14 -0700 Subject: [PATCH] Implement MCNS v1: custom Go DNS server replacing CoreDNS Replace the CoreDNS precursor with a purpose-built authoritative DNS server. Zones and records (A, AAAA, CNAME) are stored in SQLite and managed via synchronized gRPC + REST APIs authenticated through MCIAS. Non-authoritative queries are forwarded to upstream resolvers with in-memory caching. Key components: - DNS server (miekg/dns) with authoritative zone handling and forwarding - gRPC + REST management APIs with MCIAS auth (mcdsl integration) - SQLite storage with CNAME exclusivity enforcement and auto SOA serials - 30 tests covering database CRUD, DNS resolution, and caching Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 5 + .golangci.yaml | 90 ++++ Corefile | 20 - Dockerfile | 23 + Makefile | 40 ++ buf.yaml | 9 + cmd/mcns/main.go | 278 +++++++++++ deploy/docker/docker-compose-rift.yml | 19 +- deploy/examples/mcns.toml | 21 + gen/mcns/v1/admin.pb.go | 164 +++++++ gen/mcns/v1/admin_grpc.pb.go | 121 +++++ gen/mcns/v1/auth.pb.go | 279 +++++++++++ gen/mcns/v1/auth_grpc.pb.go | 159 ++++++ gen/mcns/v1/record.pb.go | 618 ++++++++++++++++++++++++ gen/mcns/v1/record_grpc.pb.go | 273 +++++++++++ gen/mcns/v1/zone.pb.go | 667 ++++++++++++++++++++++++++ gen/mcns/v1/zone_grpc.pb.go | 273 +++++++++++ go.mod | 34 ++ go.sum | 103 ++++ internal/config/config.go | 49 ++ internal/db/db.go | 23 + internal/db/migrate.go | 46 ++ internal/db/records.go | 308 ++++++++++++ internal/db/records_test.go | 289 +++++++++++ internal/db/zones.go | 182 +++++++ internal/db/zones_test.go | 167 +++++++ internal/dns/cache.go | 67 +++ internal/dns/cache_test.go | 81 ++++ internal/dns/forwarder.go | 87 ++++ internal/dns/server.go | 280 +++++++++++ internal/dns/server_test.go | 142 ++++++ internal/grpcserver/admin.go | 20 + internal/grpcserver/auth_handler.go | 38 ++ internal/grpcserver/interceptors.go | 45 ++ internal/grpcserver/records.go | 110 +++++ internal/grpcserver/server.go | 50 ++ internal/grpcserver/zones.go | 134 ++++++ internal/server/auth.go | 62 +++ internal/server/middleware.go | 96 ++++ internal/server/records.go | 174 +++++++ internal/server/routes.go | 71 +++ internal/server/zones.go | 163 +++++++ proto/mcns/v1/admin.proto | 15 + proto/mcns/v1/auth.proto | 26 + proto/mcns/v1/record.proto | 62 +++ proto/mcns/v1/zone.proto | 65 +++ zones/mcp.metacircular.net.zone | 26 - zones/svc.mcp.metacircular.net.zone | 28 -- 48 files changed, 6015 insertions(+), 87 deletions(-) create mode 100644 .gitignore create mode 100644 .golangci.yaml delete mode 100644 Corefile create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 buf.yaml create mode 100644 cmd/mcns/main.go create mode 100644 deploy/examples/mcns.toml create mode 100644 gen/mcns/v1/admin.pb.go create mode 100644 gen/mcns/v1/admin_grpc.pb.go create mode 100644 gen/mcns/v1/auth.pb.go create mode 100644 gen/mcns/v1/auth_grpc.pb.go create mode 100644 gen/mcns/v1/record.pb.go create mode 100644 gen/mcns/v1/record_grpc.pb.go create mode 100644 gen/mcns/v1/zone.pb.go create mode 100644 gen/mcns/v1/zone_grpc.pb.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/db/db.go create mode 100644 internal/db/migrate.go create mode 100644 internal/db/records.go create mode 100644 internal/db/records_test.go create mode 100644 internal/db/zones.go create mode 100644 internal/db/zones_test.go create mode 100644 internal/dns/cache.go create mode 100644 internal/dns/cache_test.go create mode 100644 internal/dns/forwarder.go create mode 100644 internal/dns/server.go create mode 100644 internal/dns/server_test.go create mode 100644 internal/grpcserver/admin.go create mode 100644 internal/grpcserver/auth_handler.go create mode 100644 internal/grpcserver/interceptors.go create mode 100644 internal/grpcserver/records.go create mode 100644 internal/grpcserver/server.go create mode 100644 internal/grpcserver/zones.go create mode 100644 internal/server/auth.go create mode 100644 internal/server/middleware.go create mode 100644 internal/server/records.go create mode 100644 internal/server/routes.go create mode 100644 internal/server/zones.go create mode 100644 proto/mcns/v1/admin.proto create mode 100644 proto/mcns/v1/auth.proto create mode 100644 proto/mcns/v1/record.proto create mode 100644 proto/mcns/v1/zone.proto delete mode 100644 zones/mcp.metacircular.net.zone delete mode 100644 zones/svc.mcp.metacircular.net.zone diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf6450f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/mcns +srv/ +*.db +*.db-wal +*.db-shm diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..e48261a --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,90 @@ +# golangci-lint v2 configuration for mcns. +# Principle: fail loudly. Security and correctness issues are errors, not warnings. + +version: "2" + +run: + timeout: 5m + tests: true + +linters: + default: none + enable: + # --- Correctness --- + - errcheck + - govet + - ineffassign + - unused + + # --- Error handling --- + - errorlint + + # --- Security --- + - gosec + - staticcheck + + # --- Style / conventions --- + - revive + + settings: + errcheck: + check-blank: false + check-type-assertions: true + + govet: + enable-all: true + disable: + - shadow + - fieldalignment + + gosec: + severity: medium + confidence: medium + excludes: + - G104 + + errorlint: + errorf: true + asserts: true + comparison: true + + revive: + rules: + - name: error-return + severity: error + - name: unexported-return + severity: error + - name: error-strings + severity: warning + - name: if-return + severity: warning + - name: increment-decrement + severity: warning + - name: var-naming + severity: warning + - name: range + severity: warning + - name: time-naming + severity: warning + - name: indent-error-flow + severity: warning + - name: early-return + severity: warning + +formatters: + enable: + - gofmt + - goimports + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + + exclusions: + paths: + - vendor + rules: + - path: "_test\\.go" + linters: + - gosec + text: "G101" diff --git a/Corefile b/Corefile deleted file mode 100644 index 555601c..0000000 --- a/Corefile +++ /dev/null @@ -1,20 +0,0 @@ -# Internal zone for Metacircular service discovery. -# Authoritative for svc.mcp.metacircular.net and mcp.metacircular.net. -# Everything else forwards to public resolvers. - -svc.mcp.metacircular.net { - file /etc/coredns/zones/svc.mcp.metacircular.net.zone - log -} - -mcp.metacircular.net { - file /etc/coredns/zones/mcp.metacircular.net.zone - log -} - -. { - forward . 1.1.1.1 8.8.8.8 - cache 30 - log - errors -} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a610760 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.25-alpine AS builder + +ARG VERSION=dev + +RUN apk add --no-cache git + +WORKDIR /build +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o mcns ./cmd/mcns + +FROM alpine:3.21 + +RUN addgroup -S mcns && adduser -S mcns -G mcns +COPY --from=builder /build/mcns /usr/local/bin/mcns + +USER mcns +EXPOSE 53/udp 53/tcp 8443 9443 + +ENTRYPOINT ["mcns"] +CMD ["server", "--config", "/srv/mcns/mcns.toml"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d3c064e --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +.PHONY: build test vet lint proto proto-lint clean docker all devserver + +LDFLAGS := -trimpath -ldflags="-s -w -X main.version=$(shell git describe --tags --always --dirty)" + +mcns: + CGO_ENABLED=0 go build $(LDFLAGS) -o mcns ./cmd/mcns + +build: + go build ./... + +test: + go test ./... + +vet: + go vet ./... + +lint: + golangci-lint run ./... + +proto: + protoc --go_out=. --go_opt=module=git.wntrmute.dev/kyle/mcns \ + --go-grpc_out=. --go-grpc_opt=module=git.wntrmute.dev/kyle/mcns \ + proto/mcns/v1/*.proto + +proto-lint: + buf lint + buf breaking --against '.git#branch=master,subdir=proto' + +clean: + rm -f mcns + +docker: + docker build --build-arg VERSION=$(shell git describe --tags --always --dirty) -t mcns -f Dockerfile . + +devserver: mcns + @mkdir -p srv + @if [ ! -f srv/mcns.toml ]; then cp deploy/examples/mcns.toml srv/mcns.toml; echo "Created srv/mcns.toml from example — edit before running."; fi + ./mcns server --config srv/mcns.toml + +all: vet lint test mcns diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..c7e30e3 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: proto +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/cmd/mcns/main.go b/cmd/mcns/main.go new file mode 100644 index 0000000..f28cdc9 --- /dev/null +++ b/cmd/mcns/main.go @@ -0,0 +1,278 @@ +package main + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "log/slog" + "net" + "net/http" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "github.com/spf13/cobra" + + mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth" + mcdsldb "git.wntrmute.dev/kyle/mcdsl/db" + + "git.wntrmute.dev/kyle/mcns/internal/config" + "git.wntrmute.dev/kyle/mcns/internal/db" + mcnsdns "git.wntrmute.dev/kyle/mcns/internal/dns" + "git.wntrmute.dev/kyle/mcns/internal/grpcserver" + "git.wntrmute.dev/kyle/mcns/internal/server" +) + +var version = "dev" + +func main() { + root := &cobra.Command{ + Use: "mcns", + Short: "Metacircular Networking Service", + Version: version, + } + + root.AddCommand(serverCmd()) + root.AddCommand(statusCmd()) + root.AddCommand(snapshotCmd()) + + if err := root.Execute(); err != nil { + os.Exit(1) + } +} + +func serverCmd() *cobra.Command { + var configPath string + + cmd := &cobra.Command{ + Use: "server", + Short: "Start the DNS and API servers", + RunE: func(_ *cobra.Command, _ []string) error { + return runServer(configPath) + }, + } + + cmd.Flags().StringVarP(&configPath, "config", "c", "mcns.toml", "path to configuration file") + return cmd +} + +func runServer(configPath string) error { + cfg, err := config.Load(configPath) + if err != nil { + return fmt.Errorf("load config: %w", err) + } + + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: parseLogLevel(cfg.Log.Level), + })) + + // Open and migrate the database. + database, err := db.Open(cfg.Database.Path) + if err != nil { + return fmt.Errorf("open database: %w", err) + } + defer database.Close() + + if err := database.Migrate(); err != nil { + return fmt.Errorf("migrate database: %w", err) + } + + // Create auth client for MCIAS integration. + authClient, err := mcdslauth.New(mcdslauth.Config{ + ServerURL: cfg.MCIAS.ServerURL, + CACert: cfg.MCIAS.CACert, + ServiceName: cfg.MCIAS.ServiceName, + Tags: cfg.MCIAS.Tags, + }, logger) + if err != nil { + return fmt.Errorf("create auth client: %w", err) + } + + // Start DNS server. + dnsServer := mcnsdns.New(database, cfg.DNS.Upstreams, logger) + + // Build REST API router. + router := server.NewRouter(server.Deps{ + DB: database, + Auth: authClient, + Logger: logger, + }) + + // TLS configuration. + cert, err := tls.LoadX509KeyPair(cfg.Server.TLSCert, cfg.Server.TLSKey) + if err != nil { + return fmt.Errorf("load TLS cert: %w", err) + } + tlsCfg := &tls.Config{ + MinVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{cert}, + } + + // HTTP server. + httpServer := &http.Server{ + Addr: cfg.Server.ListenAddr, + Handler: router, + TLSConfig: tlsCfg, + ReadTimeout: cfg.Server.ReadTimeout.Duration, + WriteTimeout: cfg.Server.WriteTimeout.Duration, + IdleTimeout: cfg.Server.IdleTimeout.Duration, + } + + // Start gRPC server if configured. + var grpcSrv *grpcserver.Server + var grpcLis net.Listener + if cfg.Server.GRPCAddr != "" { + grpcSrv, err = grpcserver.New(cfg.Server.TLSCert, cfg.Server.TLSKey, grpcserver.Deps{ + DB: database, + Authenticator: authClient, + }, logger) + if err != nil { + return fmt.Errorf("create gRPC server: %w", err) + } + grpcLis, err = net.Listen("tcp", cfg.Server.GRPCAddr) + if err != nil { + return fmt.Errorf("listen gRPC on %s: %w", cfg.Server.GRPCAddr, err) + } + } + + // Graceful shutdown on SIGINT/SIGTERM. + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + errCh := make(chan error, 3) + + // Start DNS server. + go func() { + errCh <- dnsServer.ListenAndServe(cfg.DNS.ListenAddr) + }() + + // Start gRPC server. + if grpcSrv != nil { + go func() { + logger.Info("gRPC server listening", "addr", grpcLis.Addr()) + errCh <- grpcSrv.Serve(grpcLis) + }() + } + + // Start HTTP server. + go func() { + logger.Info("mcns starting", + "version", version, + "addr", cfg.Server.ListenAddr, + "dns_addr", cfg.DNS.ListenAddr, + ) + errCh <- httpServer.ListenAndServeTLS("", "") + }() + + select { + case err := <-errCh: + return fmt.Errorf("server error: %w", err) + case <-ctx.Done(): + logger.Info("shutting down") + dnsServer.Shutdown() + if grpcSrv != nil { + grpcSrv.GracefulStop() + } + shutdownTimeout := 30 * time.Second + if cfg.Server.ShutdownTimeout.Duration > 0 { + shutdownTimeout = cfg.Server.ShutdownTimeout.Duration + } + shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + defer cancel() + if err := httpServer.Shutdown(shutdownCtx); err != nil { + return fmt.Errorf("shutdown: %w", err) + } + logger.Info("mcns stopped") + return nil + } +} + +func statusCmd() *cobra.Command { + var addr, caCert string + + cmd := &cobra.Command{ + Use: "status", + Short: "Check MCNS health", + RunE: func(_ *cobra.Command, _ []string) error { + tlsCfg := &tls.Config{MinVersion: tls.VersionTLS13} + if caCert != "" { + pemData, err := os.ReadFile(caCert) + if err != nil { + return fmt.Errorf("read CA cert: %w", err) + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(pemData) { + return fmt.Errorf("no valid certificates in %s", caCert) + } + tlsCfg.RootCAs = pool + } + client := &http.Client{ + Transport: &http.Transport{TLSClientConfig: tlsCfg}, + Timeout: 5 * time.Second, + } + resp, err := client.Get(addr + "/v1/health") + if err != nil { + return fmt.Errorf("health check: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("health check: status %d", resp.StatusCode) + } + fmt.Println("ok") + return nil + }, + } + + cmd.Flags().StringVar(&addr, "addr", "https://localhost:8443", "server address") + cmd.Flags().StringVar(&caCert, "ca-cert", "", "CA certificate for TLS verification") + return cmd +} + +func snapshotCmd() *cobra.Command { + var configPath string + + cmd := &cobra.Command{ + Use: "snapshot", + Short: "Database backup via VACUUM INTO", + RunE: func(_ *cobra.Command, _ []string) error { + cfg, err := config.Load(configPath) + if err != nil { + return fmt.Errorf("load config: %w", err) + } + database, err := db.Open(cfg.Database.Path) + if err != nil { + return fmt.Errorf("open database: %w", err) + } + defer database.Close() + + backupDir := filepath.Join(filepath.Dir(cfg.Database.Path), "backups") + snapName := fmt.Sprintf("mcns-%s.db", time.Now().Format("20060102-150405")) + snapPath := filepath.Join(backupDir, snapName) + + if err := mcdsldb.Snapshot(database.DB, snapPath); err != nil { + return fmt.Errorf("snapshot: %w", err) + } + fmt.Printf("Snapshot saved to %s\n", snapPath) + return nil + }, + } + + cmd.Flags().StringVarP(&configPath, "config", "c", "mcns.toml", "path to configuration file") + return cmd +} + +func parseLogLevel(s string) slog.Level { + switch s { + case "debug": + return slog.LevelDebug + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} diff --git a/deploy/docker/docker-compose-rift.yml b/deploy/docker/docker-compose-rift.yml index 5063e54..51e5281 100644 --- a/deploy/docker/docker-compose-rift.yml +++ b/deploy/docker/docker-compose-rift.yml @@ -1,25 +1,18 @@ -# CoreDNS on rift — MCNS precursor. -# -# Serves the svc.mcp.metacircular.net and mcp.metacircular.net zones. -# Forwards everything else to 1.1.1.1 and 8.8.8.8. +# MCNS on rift — authoritative DNS + management API. # # Usage: # docker compose -f deploy/docker/docker-compose-rift.yml up -d -# -# To use as the network's DNS server, point clients or the router at -# rift's IP (192.168.88.181) on port 53. services: - coredns: - image: coredns/coredns:1.12.1 - container_name: mcns-coredns + mcns: + image: mcr.svc.mcp.metacircular.net:8443/mcns:latest + container_name: mcns restart: unless-stopped - command: -conf /etc/coredns/Corefile + command: ["server", "--config", "/srv/mcns/mcns.toml"] ports: - "192.168.88.181:53:53/udp" - "192.168.88.181:53:53/tcp" - "100.95.252.120:53:53/udp" - "100.95.252.120:53:53/tcp" volumes: - - ../../Corefile:/etc/coredns/Corefile:ro - - ../../zones:/etc/coredns/zones:ro + - /srv/mcns:/srv/mcns diff --git a/deploy/examples/mcns.toml b/deploy/examples/mcns.toml new file mode 100644 index 0000000..59b437d --- /dev/null +++ b/deploy/examples/mcns.toml @@ -0,0 +1,21 @@ +[server] +listen_addr = ":8443" +grpc_addr = ":9443" +tls_cert = "/srv/mcns/certs/cert.pem" +tls_key = "/srv/mcns/certs/key.pem" + +[database] +path = "/srv/mcns/mcns.db" + +[dns] +listen_addr = ":53" +upstreams = ["1.1.1.1:53", "8.8.8.8:53"] + +[mcias] +server_url = "https://svc.metacircular.net:8443" +ca_cert = "" +service_name = "mcns" +tags = [] + +[log] +level = "info" diff --git a/gen/mcns/v1/admin.pb.go b/gen/mcns/v1/admin.pb.go new file mode 100644 index 0000000..35d7d14 --- /dev/null +++ b/gen/mcns/v1/admin.pb.go @@ -0,0 +1,164 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.32.1 +// source: proto/mcns/v1/admin.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type HealthRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthRequest) Reset() { + *x = HealthRequest{} + mi := &file_proto_mcns_v1_admin_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthRequest) ProtoMessage() {} + +func (x *HealthRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_admin_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthRequest.ProtoReflect.Descriptor instead. +func (*HealthRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_admin_proto_rawDescGZIP(), []int{0} +} + +type HealthResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthResponse) Reset() { + *x = HealthResponse{} + mi := &file_proto_mcns_v1_admin_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthResponse) ProtoMessage() {} + +func (x *HealthResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_admin_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthResponse.ProtoReflect.Descriptor instead. +func (*HealthResponse) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_admin_proto_rawDescGZIP(), []int{1} +} + +func (x *HealthResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +var File_proto_mcns_v1_admin_proto protoreflect.FileDescriptor + +const file_proto_mcns_v1_admin_proto_rawDesc = "" + + "\n" + + "\x19proto/mcns/v1/admin.proto\x12\amcns.v1\"\x0f\n" + + "\rHealthRequest\"(\n" + + "\x0eHealthResponse\x12\x16\n" + + "\x06status\x18\x01 \x01(\tR\x06status2I\n" + + "\fAdminService\x129\n" + + "\x06Health\x12\x16.mcns.v1.HealthRequest\x1a\x17.mcns.v1.HealthResponseB(Z&git.wntrmute.dev/kyle/mcns/gen/mcns/v1b\x06proto3" + +var ( + file_proto_mcns_v1_admin_proto_rawDescOnce sync.Once + file_proto_mcns_v1_admin_proto_rawDescData []byte +) + +func file_proto_mcns_v1_admin_proto_rawDescGZIP() []byte { + file_proto_mcns_v1_admin_proto_rawDescOnce.Do(func() { + file_proto_mcns_v1_admin_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_admin_proto_rawDesc), len(file_proto_mcns_v1_admin_proto_rawDesc))) + }) + return file_proto_mcns_v1_admin_proto_rawDescData +} + +var file_proto_mcns_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proto_mcns_v1_admin_proto_goTypes = []any{ + (*HealthRequest)(nil), // 0: mcns.v1.HealthRequest + (*HealthResponse)(nil), // 1: mcns.v1.HealthResponse +} +var file_proto_mcns_v1_admin_proto_depIdxs = []int32{ + 0, // 0: mcns.v1.AdminService.Health:input_type -> mcns.v1.HealthRequest + 1, // 1: mcns.v1.AdminService.Health:output_type -> mcns.v1.HealthResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proto_mcns_v1_admin_proto_init() } +func file_proto_mcns_v1_admin_proto_init() { + if File_proto_mcns_v1_admin_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_admin_proto_rawDesc), len(file_proto_mcns_v1_admin_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_mcns_v1_admin_proto_goTypes, + DependencyIndexes: file_proto_mcns_v1_admin_proto_depIdxs, + MessageInfos: file_proto_mcns_v1_admin_proto_msgTypes, + }.Build() + File_proto_mcns_v1_admin_proto = out.File + file_proto_mcns_v1_admin_proto_goTypes = nil + file_proto_mcns_v1_admin_proto_depIdxs = nil +} diff --git a/gen/mcns/v1/admin_grpc.pb.go b/gen/mcns/v1/admin_grpc.pb.go new file mode 100644 index 0000000..0734a50 --- /dev/null +++ b/gen/mcns/v1/admin_grpc.pb.go @@ -0,0 +1,121 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v6.32.1 +// source: proto/mcns/v1/admin.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + AdminService_Health_FullMethodName = "/mcns.v1.AdminService/Health" +) + +// AdminServiceClient is the client API for AdminService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AdminServiceClient interface { + Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error) +} + +type adminServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAdminServiceClient(cc grpc.ClientConnInterface) AdminServiceClient { + return &adminServiceClient{cc} +} + +func (c *adminServiceClient) Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HealthResponse) + err := c.cc.Invoke(ctx, AdminService_Health_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AdminServiceServer is the server API for AdminService service. +// All implementations must embed UnimplementedAdminServiceServer +// for forward compatibility. +type AdminServiceServer interface { + Health(context.Context, *HealthRequest) (*HealthResponse, error) + mustEmbedUnimplementedAdminServiceServer() +} + +// UnimplementedAdminServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAdminServiceServer struct{} + +func (UnimplementedAdminServiceServer) Health(context.Context, *HealthRequest) (*HealthResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Health not implemented") +} +func (UnimplementedAdminServiceServer) mustEmbedUnimplementedAdminServiceServer() {} +func (UnimplementedAdminServiceServer) testEmbeddedByValue() {} + +// UnsafeAdminServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AdminServiceServer will +// result in compilation errors. +type UnsafeAdminServiceServer interface { + mustEmbedUnimplementedAdminServiceServer() +} + +func RegisterAdminServiceServer(s grpc.ServiceRegistrar, srv AdminServiceServer) { + // If the following call panics, it indicates UnimplementedAdminServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&AdminService_ServiceDesc, srv) +} + +func _AdminService_Health_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminServiceServer).Health(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminService_Health_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminServiceServer).Health(ctx, req.(*HealthRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// AdminService_ServiceDesc is the grpc.ServiceDesc for AdminService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AdminService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "mcns.v1.AdminService", + HandlerType: (*AdminServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Health", + Handler: _AdminService_Health_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/mcns/v1/admin.proto", +} diff --git a/gen/mcns/v1/auth.pb.go b/gen/mcns/v1/auth.pb.go new file mode 100644 index 0000000..95cf0d5 --- /dev/null +++ b/gen/mcns/v1/auth.pb.go @@ -0,0 +1,279 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.32.1 +// source: proto/mcns/v1/auth.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type LoginRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + TotpCode string `protobuf:"bytes,3,opt,name=totp_code,json=totpCode,proto3" json:"totp_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LoginRequest) Reset() { + *x = LoginRequest{} + mi := &file_proto_mcns_v1_auth_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoginRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginRequest) ProtoMessage() {} + +func (x *LoginRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_auth_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead. +func (*LoginRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_auth_proto_rawDescGZIP(), []int{0} +} + +func (x *LoginRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *LoginRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *LoginRequest) GetTotpCode() string { + if x != nil { + return x.TotpCode + } + return "" +} + +type LoginResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LoginResponse) Reset() { + *x = LoginResponse{} + mi := &file_proto_mcns_v1_auth_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoginResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginResponse) ProtoMessage() {} + +func (x *LoginResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_auth_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead. +func (*LoginResponse) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_auth_proto_rawDescGZIP(), []int{1} +} + +func (x *LoginResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +type LogoutRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LogoutRequest) Reset() { + *x = LogoutRequest{} + mi := &file_proto_mcns_v1_auth_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LogoutRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LogoutRequest) ProtoMessage() {} + +func (x *LogoutRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_auth_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead. +func (*LogoutRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_auth_proto_rawDescGZIP(), []int{2} +} + +func (x *LogoutRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +type LogoutResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LogoutResponse) Reset() { + *x = LogoutResponse{} + mi := &file_proto_mcns_v1_auth_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LogoutResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LogoutResponse) ProtoMessage() {} + +func (x *LogoutResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_auth_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LogoutResponse.ProtoReflect.Descriptor instead. +func (*LogoutResponse) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_auth_proto_rawDescGZIP(), []int{3} +} + +var File_proto_mcns_v1_auth_proto protoreflect.FileDescriptor + +const file_proto_mcns_v1_auth_proto_rawDesc = "" + + "\n" + + "\x18proto/mcns/v1/auth.proto\x12\amcns.v1\"c\n" + + "\fLoginRequest\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\x02 \x01(\tR\bpassword\x12\x1b\n" + + "\ttotp_code\x18\x03 \x01(\tR\btotpCode\"%\n" + + "\rLoginResponse\x12\x14\n" + + "\x05token\x18\x01 \x01(\tR\x05token\"%\n" + + "\rLogoutRequest\x12\x14\n" + + "\x05token\x18\x01 \x01(\tR\x05token\"\x10\n" + + "\x0eLogoutResponse2\x80\x01\n" + + "\vAuthService\x126\n" + + "\x05Login\x12\x15.mcns.v1.LoginRequest\x1a\x16.mcns.v1.LoginResponse\x129\n" + + "\x06Logout\x12\x16.mcns.v1.LogoutRequest\x1a\x17.mcns.v1.LogoutResponseB(Z&git.wntrmute.dev/kyle/mcns/gen/mcns/v1b\x06proto3" + +var ( + file_proto_mcns_v1_auth_proto_rawDescOnce sync.Once + file_proto_mcns_v1_auth_proto_rawDescData []byte +) + +func file_proto_mcns_v1_auth_proto_rawDescGZIP() []byte { + file_proto_mcns_v1_auth_proto_rawDescOnce.Do(func() { + file_proto_mcns_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_auth_proto_rawDesc), len(file_proto_mcns_v1_auth_proto_rawDesc))) + }) + return file_proto_mcns_v1_auth_proto_rawDescData +} + +var file_proto_mcns_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_proto_mcns_v1_auth_proto_goTypes = []any{ + (*LoginRequest)(nil), // 0: mcns.v1.LoginRequest + (*LoginResponse)(nil), // 1: mcns.v1.LoginResponse + (*LogoutRequest)(nil), // 2: mcns.v1.LogoutRequest + (*LogoutResponse)(nil), // 3: mcns.v1.LogoutResponse +} +var file_proto_mcns_v1_auth_proto_depIdxs = []int32{ + 0, // 0: mcns.v1.AuthService.Login:input_type -> mcns.v1.LoginRequest + 2, // 1: mcns.v1.AuthService.Logout:input_type -> mcns.v1.LogoutRequest + 1, // 2: mcns.v1.AuthService.Login:output_type -> mcns.v1.LoginResponse + 3, // 3: mcns.v1.AuthService.Logout:output_type -> mcns.v1.LogoutResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proto_mcns_v1_auth_proto_init() } +func file_proto_mcns_v1_auth_proto_init() { + if File_proto_mcns_v1_auth_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_auth_proto_rawDesc), len(file_proto_mcns_v1_auth_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_mcns_v1_auth_proto_goTypes, + DependencyIndexes: file_proto_mcns_v1_auth_proto_depIdxs, + MessageInfos: file_proto_mcns_v1_auth_proto_msgTypes, + }.Build() + File_proto_mcns_v1_auth_proto = out.File + file_proto_mcns_v1_auth_proto_goTypes = nil + file_proto_mcns_v1_auth_proto_depIdxs = nil +} diff --git a/gen/mcns/v1/auth_grpc.pb.go b/gen/mcns/v1/auth_grpc.pb.go new file mode 100644 index 0000000..ba5c146 --- /dev/null +++ b/gen/mcns/v1/auth_grpc.pb.go @@ -0,0 +1,159 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v6.32.1 +// source: proto/mcns/v1/auth.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + AuthService_Login_FullMethodName = "/mcns.v1.AuthService/Login" + AuthService_Logout_FullMethodName = "/mcns.v1.AuthService/Logout" +) + +// AuthServiceClient is the client API for AuthService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AuthServiceClient interface { + Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) + Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) +} + +type authServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient { + return &authServiceClient{cc} +} + +func (c *authServiceClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LoginResponse) + err := c.cc.Invoke(ctx, AuthService_Login_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LogoutResponse) + err := c.cc.Invoke(ctx, AuthService_Logout_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AuthServiceServer is the server API for AuthService service. +// All implementations must embed UnimplementedAuthServiceServer +// for forward compatibility. +type AuthServiceServer interface { + Login(context.Context, *LoginRequest) (*LoginResponse, error) + Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) + mustEmbedUnimplementedAuthServiceServer() +} + +// UnimplementedAuthServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAuthServiceServer struct{} + +func (UnimplementedAuthServiceServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Login not implemented") +} +func (UnimplementedAuthServiceServer) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Logout not implemented") +} +func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {} +func (UnimplementedAuthServiceServer) testEmbeddedByValue() {} + +// UnsafeAuthServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AuthServiceServer will +// result in compilation errors. +type UnsafeAuthServiceServer interface { + mustEmbedUnimplementedAuthServiceServer() +} + +func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) { + // If the following call panics, it indicates UnimplementedAuthServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&AuthService_ServiceDesc, srv) +} + +func _AuthService_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LoginRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).Login(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_Login_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).Login(ctx, req.(*LoginRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LogoutRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).Logout(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_Logout_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).Logout(ctx, req.(*LogoutRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AuthService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "mcns.v1.AuthService", + HandlerType: (*AuthServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Login", + Handler: _AuthService_Login_Handler, + }, + { + MethodName: "Logout", + Handler: _AuthService_Logout_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/mcns/v1/auth.proto", +} diff --git a/gen/mcns/v1/record.pb.go b/gen/mcns/v1/record.pb.go new file mode 100644 index 0000000..4b4c4c2 --- /dev/null +++ b/gen/mcns/v1/record.pb.go @@ -0,0 +1,618 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.32.1 +// source: proto/mcns/v1/record.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Record struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Zone string `protobuf:"bytes,2,opt,name=zone,proto3" json:"zone,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` + Value string `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"` + Ttl int32 `protobuf:"varint,6,opt,name=ttl,proto3" json:"ttl,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Record) Reset() { + *x = Record{} + mi := &file_proto_mcns_v1_record_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Record) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Record) ProtoMessage() {} + +func (x *Record) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_record_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Record.ProtoReflect.Descriptor instead. +func (*Record) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{0} +} + +func (x *Record) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Record) GetZone() string { + if x != nil { + return x.Zone + } + return "" +} + +func (x *Record) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Record) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Record) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *Record) GetTtl() int32 { + if x != nil { + return x.Ttl + } + return 0 +} + +func (x *Record) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Record) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type ListRecordsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Zone string `protobuf:"bytes,1,opt,name=zone,proto3" json:"zone,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRecordsRequest) Reset() { + *x = ListRecordsRequest{} + mi := &file_proto_mcns_v1_record_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRecordsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRecordsRequest) ProtoMessage() {} + +func (x *ListRecordsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_record_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRecordsRequest.ProtoReflect.Descriptor instead. +func (*ListRecordsRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{1} +} + +func (x *ListRecordsRequest) GetZone() string { + if x != nil { + return x.Zone + } + return "" +} + +func (x *ListRecordsRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ListRecordsRequest) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +type ListRecordsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Records []*Record `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRecordsResponse) Reset() { + *x = ListRecordsResponse{} + mi := &file_proto_mcns_v1_record_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRecordsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRecordsResponse) ProtoMessage() {} + +func (x *ListRecordsResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_record_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRecordsResponse.ProtoReflect.Descriptor instead. +func (*ListRecordsResponse) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{2} +} + +func (x *ListRecordsResponse) GetRecords() []*Record { + if x != nil { + return x.Records + } + return nil +} + +type CreateRecordRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Zone string `protobuf:"bytes,1,opt,name=zone,proto3" json:"zone,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Value string `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` + Ttl int32 `protobuf:"varint,5,opt,name=ttl,proto3" json:"ttl,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateRecordRequest) Reset() { + *x = CreateRecordRequest{} + mi := &file_proto_mcns_v1_record_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateRecordRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRecordRequest) ProtoMessage() {} + +func (x *CreateRecordRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_record_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRecordRequest.ProtoReflect.Descriptor instead. +func (*CreateRecordRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateRecordRequest) GetZone() string { + if x != nil { + return x.Zone + } + return "" +} + +func (x *CreateRecordRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateRecordRequest) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *CreateRecordRequest) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *CreateRecordRequest) GetTtl() int32 { + if x != nil { + return x.Ttl + } + return 0 +} + +type GetRecordRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRecordRequest) Reset() { + *x = GetRecordRequest{} + mi := &file_proto_mcns_v1_record_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRecordRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRecordRequest) ProtoMessage() {} + +func (x *GetRecordRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_record_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRecordRequest.ProtoReflect.Descriptor instead. +func (*GetRecordRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{4} +} + +func (x *GetRecordRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type UpdateRecordRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Value string `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` + Ttl int32 `protobuf:"varint,5,opt,name=ttl,proto3" json:"ttl,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateRecordRequest) Reset() { + *x = UpdateRecordRequest{} + mi := &file_proto_mcns_v1_record_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateRecordRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateRecordRequest) ProtoMessage() {} + +func (x *UpdateRecordRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_record_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateRecordRequest.ProtoReflect.Descriptor instead. +func (*UpdateRecordRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{5} +} + +func (x *UpdateRecordRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *UpdateRecordRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpdateRecordRequest) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *UpdateRecordRequest) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *UpdateRecordRequest) GetTtl() int32 { + if x != nil { + return x.Ttl + } + return 0 +} + +type DeleteRecordRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRecordRequest) Reset() { + *x = DeleteRecordRequest{} + mi := &file_proto_mcns_v1_record_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRecordRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRecordRequest) ProtoMessage() {} + +func (x *DeleteRecordRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_record_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRecordRequest.ProtoReflect.Descriptor instead. +func (*DeleteRecordRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{6} +} + +func (x *DeleteRecordRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type DeleteRecordResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRecordResponse) Reset() { + *x = DeleteRecordResponse{} + mi := &file_proto_mcns_v1_record_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRecordResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRecordResponse) ProtoMessage() {} + +func (x *DeleteRecordResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_record_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRecordResponse.ProtoReflect.Descriptor instead. +func (*DeleteRecordResponse) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{7} +} + +var File_proto_mcns_v1_record_proto protoreflect.FileDescriptor + +const file_proto_mcns_v1_record_proto_rawDesc = "" + + "\n" + + "\x1aproto/mcns/v1/record.proto\x12\amcns.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xf2\x01\n" + + "\x06Record\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04zone\x18\x02 \x01(\tR\x04zone\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x04 \x01(\tR\x04type\x12\x14\n" + + "\x05value\x18\x05 \x01(\tR\x05value\x12\x10\n" + + "\x03ttl\x18\x06 \x01(\x05R\x03ttl\x129\n" + + "\n" + + "created_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + + "\n" + + "updated_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"P\n" + + "\x12ListRecordsRequest\x12\x12\n" + + "\x04zone\x18\x01 \x01(\tR\x04zone\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\"@\n" + + "\x13ListRecordsResponse\x12)\n" + + "\arecords\x18\x01 \x03(\v2\x0f.mcns.v1.RecordR\arecords\"y\n" + + "\x13CreateRecordRequest\x12\x12\n" + + "\x04zone\x18\x01 \x01(\tR\x04zone\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\x12\x14\n" + + "\x05value\x18\x04 \x01(\tR\x05value\x12\x10\n" + + "\x03ttl\x18\x05 \x01(\x05R\x03ttl\"\"\n" + + "\x10GetRecordRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\"u\n" + + "\x13UpdateRecordRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\x12\x14\n" + + "\x05value\x18\x04 \x01(\tR\x05value\x12\x10\n" + + "\x03ttl\x18\x05 \x01(\x05R\x03ttl\"%\n" + + "\x13DeleteRecordRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\"\x16\n" + + "\x14DeleteRecordResponse2\xdd\x02\n" + + "\rRecordService\x12H\n" + + "\vListRecords\x12\x1b.mcns.v1.ListRecordsRequest\x1a\x1c.mcns.v1.ListRecordsResponse\x12=\n" + + "\fCreateRecord\x12\x1c.mcns.v1.CreateRecordRequest\x1a\x0f.mcns.v1.Record\x127\n" + + "\tGetRecord\x12\x19.mcns.v1.GetRecordRequest\x1a\x0f.mcns.v1.Record\x12=\n" + + "\fUpdateRecord\x12\x1c.mcns.v1.UpdateRecordRequest\x1a\x0f.mcns.v1.Record\x12K\n" + + "\fDeleteRecord\x12\x1c.mcns.v1.DeleteRecordRequest\x1a\x1d.mcns.v1.DeleteRecordResponseB(Z&git.wntrmute.dev/kyle/mcns/gen/mcns/v1b\x06proto3" + +var ( + file_proto_mcns_v1_record_proto_rawDescOnce sync.Once + file_proto_mcns_v1_record_proto_rawDescData []byte +) + +func file_proto_mcns_v1_record_proto_rawDescGZIP() []byte { + file_proto_mcns_v1_record_proto_rawDescOnce.Do(func() { + file_proto_mcns_v1_record_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_record_proto_rawDesc), len(file_proto_mcns_v1_record_proto_rawDesc))) + }) + return file_proto_mcns_v1_record_proto_rawDescData +} + +var file_proto_mcns_v1_record_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_proto_mcns_v1_record_proto_goTypes = []any{ + (*Record)(nil), // 0: mcns.v1.Record + (*ListRecordsRequest)(nil), // 1: mcns.v1.ListRecordsRequest + (*ListRecordsResponse)(nil), // 2: mcns.v1.ListRecordsResponse + (*CreateRecordRequest)(nil), // 3: mcns.v1.CreateRecordRequest + (*GetRecordRequest)(nil), // 4: mcns.v1.GetRecordRequest + (*UpdateRecordRequest)(nil), // 5: mcns.v1.UpdateRecordRequest + (*DeleteRecordRequest)(nil), // 6: mcns.v1.DeleteRecordRequest + (*DeleteRecordResponse)(nil), // 7: mcns.v1.DeleteRecordResponse + (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp +} +var file_proto_mcns_v1_record_proto_depIdxs = []int32{ + 8, // 0: mcns.v1.Record.created_at:type_name -> google.protobuf.Timestamp + 8, // 1: mcns.v1.Record.updated_at:type_name -> google.protobuf.Timestamp + 0, // 2: mcns.v1.ListRecordsResponse.records:type_name -> mcns.v1.Record + 1, // 3: mcns.v1.RecordService.ListRecords:input_type -> mcns.v1.ListRecordsRequest + 3, // 4: mcns.v1.RecordService.CreateRecord:input_type -> mcns.v1.CreateRecordRequest + 4, // 5: mcns.v1.RecordService.GetRecord:input_type -> mcns.v1.GetRecordRequest + 5, // 6: mcns.v1.RecordService.UpdateRecord:input_type -> mcns.v1.UpdateRecordRequest + 6, // 7: mcns.v1.RecordService.DeleteRecord:input_type -> mcns.v1.DeleteRecordRequest + 2, // 8: mcns.v1.RecordService.ListRecords:output_type -> mcns.v1.ListRecordsResponse + 0, // 9: mcns.v1.RecordService.CreateRecord:output_type -> mcns.v1.Record + 0, // 10: mcns.v1.RecordService.GetRecord:output_type -> mcns.v1.Record + 0, // 11: mcns.v1.RecordService.UpdateRecord:output_type -> mcns.v1.Record + 7, // 12: mcns.v1.RecordService.DeleteRecord:output_type -> mcns.v1.DeleteRecordResponse + 8, // [8:13] is the sub-list for method output_type + 3, // [3:8] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_proto_mcns_v1_record_proto_init() } +func file_proto_mcns_v1_record_proto_init() { + if File_proto_mcns_v1_record_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_record_proto_rawDesc), len(file_proto_mcns_v1_record_proto_rawDesc)), + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_mcns_v1_record_proto_goTypes, + DependencyIndexes: file_proto_mcns_v1_record_proto_depIdxs, + MessageInfos: file_proto_mcns_v1_record_proto_msgTypes, + }.Build() + File_proto_mcns_v1_record_proto = out.File + file_proto_mcns_v1_record_proto_goTypes = nil + file_proto_mcns_v1_record_proto_depIdxs = nil +} diff --git a/gen/mcns/v1/record_grpc.pb.go b/gen/mcns/v1/record_grpc.pb.go new file mode 100644 index 0000000..72e5c0b --- /dev/null +++ b/gen/mcns/v1/record_grpc.pb.go @@ -0,0 +1,273 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v6.32.1 +// source: proto/mcns/v1/record.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + RecordService_ListRecords_FullMethodName = "/mcns.v1.RecordService/ListRecords" + RecordService_CreateRecord_FullMethodName = "/mcns.v1.RecordService/CreateRecord" + RecordService_GetRecord_FullMethodName = "/mcns.v1.RecordService/GetRecord" + RecordService_UpdateRecord_FullMethodName = "/mcns.v1.RecordService/UpdateRecord" + RecordService_DeleteRecord_FullMethodName = "/mcns.v1.RecordService/DeleteRecord" +) + +// RecordServiceClient is the client API for RecordService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type RecordServiceClient interface { + ListRecords(ctx context.Context, in *ListRecordsRequest, opts ...grpc.CallOption) (*ListRecordsResponse, error) + CreateRecord(ctx context.Context, in *CreateRecordRequest, opts ...grpc.CallOption) (*Record, error) + GetRecord(ctx context.Context, in *GetRecordRequest, opts ...grpc.CallOption) (*Record, error) + UpdateRecord(ctx context.Context, in *UpdateRecordRequest, opts ...grpc.CallOption) (*Record, error) + DeleteRecord(ctx context.Context, in *DeleteRecordRequest, opts ...grpc.CallOption) (*DeleteRecordResponse, error) +} + +type recordServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewRecordServiceClient(cc grpc.ClientConnInterface) RecordServiceClient { + return &recordServiceClient{cc} +} + +func (c *recordServiceClient) ListRecords(ctx context.Context, in *ListRecordsRequest, opts ...grpc.CallOption) (*ListRecordsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRecordsResponse) + err := c.cc.Invoke(ctx, RecordService_ListRecords_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *recordServiceClient) CreateRecord(ctx context.Context, in *CreateRecordRequest, opts ...grpc.CallOption) (*Record, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Record) + err := c.cc.Invoke(ctx, RecordService_CreateRecord_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *recordServiceClient) GetRecord(ctx context.Context, in *GetRecordRequest, opts ...grpc.CallOption) (*Record, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Record) + err := c.cc.Invoke(ctx, RecordService_GetRecord_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *recordServiceClient) UpdateRecord(ctx context.Context, in *UpdateRecordRequest, opts ...grpc.CallOption) (*Record, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Record) + err := c.cc.Invoke(ctx, RecordService_UpdateRecord_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *recordServiceClient) DeleteRecord(ctx context.Context, in *DeleteRecordRequest, opts ...grpc.CallOption) (*DeleteRecordResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteRecordResponse) + err := c.cc.Invoke(ctx, RecordService_DeleteRecord_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// RecordServiceServer is the server API for RecordService service. +// All implementations must embed UnimplementedRecordServiceServer +// for forward compatibility. +type RecordServiceServer interface { + ListRecords(context.Context, *ListRecordsRequest) (*ListRecordsResponse, error) + CreateRecord(context.Context, *CreateRecordRequest) (*Record, error) + GetRecord(context.Context, *GetRecordRequest) (*Record, error) + UpdateRecord(context.Context, *UpdateRecordRequest) (*Record, error) + DeleteRecord(context.Context, *DeleteRecordRequest) (*DeleteRecordResponse, error) + mustEmbedUnimplementedRecordServiceServer() +} + +// UnimplementedRecordServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedRecordServiceServer struct{} + +func (UnimplementedRecordServiceServer) ListRecords(context.Context, *ListRecordsRequest) (*ListRecordsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRecords not implemented") +} +func (UnimplementedRecordServiceServer) CreateRecord(context.Context, *CreateRecordRequest) (*Record, error) { + return nil, status.Error(codes.Unimplemented, "method CreateRecord not implemented") +} +func (UnimplementedRecordServiceServer) GetRecord(context.Context, *GetRecordRequest) (*Record, error) { + return nil, status.Error(codes.Unimplemented, "method GetRecord not implemented") +} +func (UnimplementedRecordServiceServer) UpdateRecord(context.Context, *UpdateRecordRequest) (*Record, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateRecord not implemented") +} +func (UnimplementedRecordServiceServer) DeleteRecord(context.Context, *DeleteRecordRequest) (*DeleteRecordResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteRecord not implemented") +} +func (UnimplementedRecordServiceServer) mustEmbedUnimplementedRecordServiceServer() {} +func (UnimplementedRecordServiceServer) testEmbeddedByValue() {} + +// UnsafeRecordServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to RecordServiceServer will +// result in compilation errors. +type UnsafeRecordServiceServer interface { + mustEmbedUnimplementedRecordServiceServer() +} + +func RegisterRecordServiceServer(s grpc.ServiceRegistrar, srv RecordServiceServer) { + // If the following call panics, it indicates UnimplementedRecordServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&RecordService_ServiceDesc, srv) +} + +func _RecordService_ListRecords_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRecordsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RecordServiceServer).ListRecords(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RecordService_ListRecords_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RecordServiceServer).ListRecords(ctx, req.(*ListRecordsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RecordService_CreateRecord_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateRecordRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RecordServiceServer).CreateRecord(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RecordService_CreateRecord_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RecordServiceServer).CreateRecord(ctx, req.(*CreateRecordRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RecordService_GetRecord_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRecordRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RecordServiceServer).GetRecord(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RecordService_GetRecord_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RecordServiceServer).GetRecord(ctx, req.(*GetRecordRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RecordService_UpdateRecord_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateRecordRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RecordServiceServer).UpdateRecord(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RecordService_UpdateRecord_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RecordServiceServer).UpdateRecord(ctx, req.(*UpdateRecordRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RecordService_DeleteRecord_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteRecordRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RecordServiceServer).DeleteRecord(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RecordService_DeleteRecord_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RecordServiceServer).DeleteRecord(ctx, req.(*DeleteRecordRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// RecordService_ServiceDesc is the grpc.ServiceDesc for RecordService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var RecordService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "mcns.v1.RecordService", + HandlerType: (*RecordServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListRecords", + Handler: _RecordService_ListRecords_Handler, + }, + { + MethodName: "CreateRecord", + Handler: _RecordService_CreateRecord_Handler, + }, + { + MethodName: "GetRecord", + Handler: _RecordService_GetRecord_Handler, + }, + { + MethodName: "UpdateRecord", + Handler: _RecordService_UpdateRecord_Handler, + }, + { + MethodName: "DeleteRecord", + Handler: _RecordService_DeleteRecord_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/mcns/v1/record.proto", +} diff --git a/gen/mcns/v1/zone.pb.go b/gen/mcns/v1/zone.pb.go new file mode 100644 index 0000000..6e155c2 --- /dev/null +++ b/gen/mcns/v1/zone.pb.go @@ -0,0 +1,667 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.32.1 +// source: proto/mcns/v1/zone.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Zone struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + PrimaryNs string `protobuf:"bytes,3,opt,name=primary_ns,json=primaryNs,proto3" json:"primary_ns,omitempty"` + AdminEmail string `protobuf:"bytes,4,opt,name=admin_email,json=adminEmail,proto3" json:"admin_email,omitempty"` + Refresh int32 `protobuf:"varint,5,opt,name=refresh,proto3" json:"refresh,omitempty"` + Retry int32 `protobuf:"varint,6,opt,name=retry,proto3" json:"retry,omitempty"` + Expire int32 `protobuf:"varint,7,opt,name=expire,proto3" json:"expire,omitempty"` + MinimumTtl int32 `protobuf:"varint,8,opt,name=minimum_ttl,json=minimumTtl,proto3" json:"minimum_ttl,omitempty"` + Serial int64 `protobuf:"varint,9,opt,name=serial,proto3" json:"serial,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Zone) Reset() { + *x = Zone{} + mi := &file_proto_mcns_v1_zone_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Zone) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Zone) ProtoMessage() {} + +func (x *Zone) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_zone_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Zone.ProtoReflect.Descriptor instead. +func (*Zone) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{0} +} + +func (x *Zone) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Zone) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Zone) GetPrimaryNs() string { + if x != nil { + return x.PrimaryNs + } + return "" +} + +func (x *Zone) GetAdminEmail() string { + if x != nil { + return x.AdminEmail + } + return "" +} + +func (x *Zone) GetRefresh() int32 { + if x != nil { + return x.Refresh + } + return 0 +} + +func (x *Zone) GetRetry() int32 { + if x != nil { + return x.Retry + } + return 0 +} + +func (x *Zone) GetExpire() int32 { + if x != nil { + return x.Expire + } + return 0 +} + +func (x *Zone) GetMinimumTtl() int32 { + if x != nil { + return x.MinimumTtl + } + return 0 +} + +func (x *Zone) GetSerial() int64 { + if x != nil { + return x.Serial + } + return 0 +} + +func (x *Zone) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Zone) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type ListZonesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListZonesRequest) Reset() { + *x = ListZonesRequest{} + mi := &file_proto_mcns_v1_zone_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListZonesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListZonesRequest) ProtoMessage() {} + +func (x *ListZonesRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_zone_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListZonesRequest.ProtoReflect.Descriptor instead. +func (*ListZonesRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{1} +} + +type ListZonesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Zones []*Zone `protobuf:"bytes,1,rep,name=zones,proto3" json:"zones,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListZonesResponse) Reset() { + *x = ListZonesResponse{} + mi := &file_proto_mcns_v1_zone_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListZonesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListZonesResponse) ProtoMessage() {} + +func (x *ListZonesResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_zone_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListZonesResponse.ProtoReflect.Descriptor instead. +func (*ListZonesResponse) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{2} +} + +func (x *ListZonesResponse) GetZones() []*Zone { + if x != nil { + return x.Zones + } + return nil +} + +type CreateZoneRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + PrimaryNs string `protobuf:"bytes,2,opt,name=primary_ns,json=primaryNs,proto3" json:"primary_ns,omitempty"` + AdminEmail string `protobuf:"bytes,3,opt,name=admin_email,json=adminEmail,proto3" json:"admin_email,omitempty"` + Refresh int32 `protobuf:"varint,4,opt,name=refresh,proto3" json:"refresh,omitempty"` + Retry int32 `protobuf:"varint,5,opt,name=retry,proto3" json:"retry,omitempty"` + Expire int32 `protobuf:"varint,6,opt,name=expire,proto3" json:"expire,omitempty"` + MinimumTtl int32 `protobuf:"varint,7,opt,name=minimum_ttl,json=minimumTtl,proto3" json:"minimum_ttl,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateZoneRequest) Reset() { + *x = CreateZoneRequest{} + mi := &file_proto_mcns_v1_zone_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateZoneRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateZoneRequest) ProtoMessage() {} + +func (x *CreateZoneRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_zone_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateZoneRequest.ProtoReflect.Descriptor instead. +func (*CreateZoneRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateZoneRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateZoneRequest) GetPrimaryNs() string { + if x != nil { + return x.PrimaryNs + } + return "" +} + +func (x *CreateZoneRequest) GetAdminEmail() string { + if x != nil { + return x.AdminEmail + } + return "" +} + +func (x *CreateZoneRequest) GetRefresh() int32 { + if x != nil { + return x.Refresh + } + return 0 +} + +func (x *CreateZoneRequest) GetRetry() int32 { + if x != nil { + return x.Retry + } + return 0 +} + +func (x *CreateZoneRequest) GetExpire() int32 { + if x != nil { + return x.Expire + } + return 0 +} + +func (x *CreateZoneRequest) GetMinimumTtl() int32 { + if x != nil { + return x.MinimumTtl + } + return 0 +} + +type GetZoneRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetZoneRequest) Reset() { + *x = GetZoneRequest{} + mi := &file_proto_mcns_v1_zone_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetZoneRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetZoneRequest) ProtoMessage() {} + +func (x *GetZoneRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_zone_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetZoneRequest.ProtoReflect.Descriptor instead. +func (*GetZoneRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{4} +} + +func (x *GetZoneRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type UpdateZoneRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + PrimaryNs string `protobuf:"bytes,2,opt,name=primary_ns,json=primaryNs,proto3" json:"primary_ns,omitempty"` + AdminEmail string `protobuf:"bytes,3,opt,name=admin_email,json=adminEmail,proto3" json:"admin_email,omitempty"` + Refresh int32 `protobuf:"varint,4,opt,name=refresh,proto3" json:"refresh,omitempty"` + Retry int32 `protobuf:"varint,5,opt,name=retry,proto3" json:"retry,omitempty"` + Expire int32 `protobuf:"varint,6,opt,name=expire,proto3" json:"expire,omitempty"` + MinimumTtl int32 `protobuf:"varint,7,opt,name=minimum_ttl,json=minimumTtl,proto3" json:"minimum_ttl,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateZoneRequest) Reset() { + *x = UpdateZoneRequest{} + mi := &file_proto_mcns_v1_zone_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateZoneRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateZoneRequest) ProtoMessage() {} + +func (x *UpdateZoneRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_zone_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateZoneRequest.ProtoReflect.Descriptor instead. +func (*UpdateZoneRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{5} +} + +func (x *UpdateZoneRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpdateZoneRequest) GetPrimaryNs() string { + if x != nil { + return x.PrimaryNs + } + return "" +} + +func (x *UpdateZoneRequest) GetAdminEmail() string { + if x != nil { + return x.AdminEmail + } + return "" +} + +func (x *UpdateZoneRequest) GetRefresh() int32 { + if x != nil { + return x.Refresh + } + return 0 +} + +func (x *UpdateZoneRequest) GetRetry() int32 { + if x != nil { + return x.Retry + } + return 0 +} + +func (x *UpdateZoneRequest) GetExpire() int32 { + if x != nil { + return x.Expire + } + return 0 +} + +func (x *UpdateZoneRequest) GetMinimumTtl() int32 { + if x != nil { + return x.MinimumTtl + } + return 0 +} + +type DeleteZoneRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteZoneRequest) Reset() { + *x = DeleteZoneRequest{} + mi := &file_proto_mcns_v1_zone_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteZoneRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteZoneRequest) ProtoMessage() {} + +func (x *DeleteZoneRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_zone_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteZoneRequest.ProtoReflect.Descriptor instead. +func (*DeleteZoneRequest) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{6} +} + +func (x *DeleteZoneRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type DeleteZoneResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteZoneResponse) Reset() { + *x = DeleteZoneResponse{} + mi := &file_proto_mcns_v1_zone_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteZoneResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteZoneResponse) ProtoMessage() {} + +func (x *DeleteZoneResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_mcns_v1_zone_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteZoneResponse.ProtoReflect.Descriptor instead. +func (*DeleteZoneResponse) Descriptor() ([]byte, []int) { + return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{7} +} + +var File_proto_mcns_v1_zone_proto protoreflect.FileDescriptor + +const file_proto_mcns_v1_zone_proto_rawDesc = "" + + "\n" + + "\x18proto/mcns/v1/zone.proto\x12\amcns.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe1\x02\n" + + "\x04Zone\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1d\n" + + "\n" + + "primary_ns\x18\x03 \x01(\tR\tprimaryNs\x12\x1f\n" + + "\vadmin_email\x18\x04 \x01(\tR\n" + + "adminEmail\x12\x18\n" + + "\arefresh\x18\x05 \x01(\x05R\arefresh\x12\x14\n" + + "\x05retry\x18\x06 \x01(\x05R\x05retry\x12\x16\n" + + "\x06expire\x18\a \x01(\x05R\x06expire\x12\x1f\n" + + "\vminimum_ttl\x18\b \x01(\x05R\n" + + "minimumTtl\x12\x16\n" + + "\x06serial\x18\t \x01(\x03R\x06serial\x129\n" + + "\n" + + "created_at\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + + "\n" + + "updated_at\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\x12\n" + + "\x10ListZonesRequest\"8\n" + + "\x11ListZonesResponse\x12#\n" + + "\x05zones\x18\x01 \x03(\v2\r.mcns.v1.ZoneR\x05zones\"\xd0\x01\n" + + "\x11CreateZoneRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + + "\n" + + "primary_ns\x18\x02 \x01(\tR\tprimaryNs\x12\x1f\n" + + "\vadmin_email\x18\x03 \x01(\tR\n" + + "adminEmail\x12\x18\n" + + "\arefresh\x18\x04 \x01(\x05R\arefresh\x12\x14\n" + + "\x05retry\x18\x05 \x01(\x05R\x05retry\x12\x16\n" + + "\x06expire\x18\x06 \x01(\x05R\x06expire\x12\x1f\n" + + "\vminimum_ttl\x18\a \x01(\x05R\n" + + "minimumTtl\"$\n" + + "\x0eGetZoneRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\xd0\x01\n" + + "\x11UpdateZoneRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + + "\n" + + "primary_ns\x18\x02 \x01(\tR\tprimaryNs\x12\x1f\n" + + "\vadmin_email\x18\x03 \x01(\tR\n" + + "adminEmail\x12\x18\n" + + "\arefresh\x18\x04 \x01(\x05R\arefresh\x12\x14\n" + + "\x05retry\x18\x05 \x01(\x05R\x05retry\x12\x16\n" + + "\x06expire\x18\x06 \x01(\x05R\x06expire\x12\x1f\n" + + "\vminimum_ttl\x18\a \x01(\x05R\n" + + "minimumTtl\"'\n" + + "\x11DeleteZoneRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\x14\n" + + "\x12DeleteZoneResponse2\xbd\x02\n" + + "\vZoneService\x12B\n" + + "\tListZones\x12\x19.mcns.v1.ListZonesRequest\x1a\x1a.mcns.v1.ListZonesResponse\x127\n" + + "\n" + + "CreateZone\x12\x1a.mcns.v1.CreateZoneRequest\x1a\r.mcns.v1.Zone\x121\n" + + "\aGetZone\x12\x17.mcns.v1.GetZoneRequest\x1a\r.mcns.v1.Zone\x127\n" + + "\n" + + "UpdateZone\x12\x1a.mcns.v1.UpdateZoneRequest\x1a\r.mcns.v1.Zone\x12E\n" + + "\n" + + "DeleteZone\x12\x1a.mcns.v1.DeleteZoneRequest\x1a\x1b.mcns.v1.DeleteZoneResponseB(Z&git.wntrmute.dev/kyle/mcns/gen/mcns/v1b\x06proto3" + +var ( + file_proto_mcns_v1_zone_proto_rawDescOnce sync.Once + file_proto_mcns_v1_zone_proto_rawDescData []byte +) + +func file_proto_mcns_v1_zone_proto_rawDescGZIP() []byte { + file_proto_mcns_v1_zone_proto_rawDescOnce.Do(func() { + file_proto_mcns_v1_zone_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_zone_proto_rawDesc), len(file_proto_mcns_v1_zone_proto_rawDesc))) + }) + return file_proto_mcns_v1_zone_proto_rawDescData +} + +var file_proto_mcns_v1_zone_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_proto_mcns_v1_zone_proto_goTypes = []any{ + (*Zone)(nil), // 0: mcns.v1.Zone + (*ListZonesRequest)(nil), // 1: mcns.v1.ListZonesRequest + (*ListZonesResponse)(nil), // 2: mcns.v1.ListZonesResponse + (*CreateZoneRequest)(nil), // 3: mcns.v1.CreateZoneRequest + (*GetZoneRequest)(nil), // 4: mcns.v1.GetZoneRequest + (*UpdateZoneRequest)(nil), // 5: mcns.v1.UpdateZoneRequest + (*DeleteZoneRequest)(nil), // 6: mcns.v1.DeleteZoneRequest + (*DeleteZoneResponse)(nil), // 7: mcns.v1.DeleteZoneResponse + (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp +} +var file_proto_mcns_v1_zone_proto_depIdxs = []int32{ + 8, // 0: mcns.v1.Zone.created_at:type_name -> google.protobuf.Timestamp + 8, // 1: mcns.v1.Zone.updated_at:type_name -> google.protobuf.Timestamp + 0, // 2: mcns.v1.ListZonesResponse.zones:type_name -> mcns.v1.Zone + 1, // 3: mcns.v1.ZoneService.ListZones:input_type -> mcns.v1.ListZonesRequest + 3, // 4: mcns.v1.ZoneService.CreateZone:input_type -> mcns.v1.CreateZoneRequest + 4, // 5: mcns.v1.ZoneService.GetZone:input_type -> mcns.v1.GetZoneRequest + 5, // 6: mcns.v1.ZoneService.UpdateZone:input_type -> mcns.v1.UpdateZoneRequest + 6, // 7: mcns.v1.ZoneService.DeleteZone:input_type -> mcns.v1.DeleteZoneRequest + 2, // 8: mcns.v1.ZoneService.ListZones:output_type -> mcns.v1.ListZonesResponse + 0, // 9: mcns.v1.ZoneService.CreateZone:output_type -> mcns.v1.Zone + 0, // 10: mcns.v1.ZoneService.GetZone:output_type -> mcns.v1.Zone + 0, // 11: mcns.v1.ZoneService.UpdateZone:output_type -> mcns.v1.Zone + 7, // 12: mcns.v1.ZoneService.DeleteZone:output_type -> mcns.v1.DeleteZoneResponse + 8, // [8:13] is the sub-list for method output_type + 3, // [3:8] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_proto_mcns_v1_zone_proto_init() } +func file_proto_mcns_v1_zone_proto_init() { + if File_proto_mcns_v1_zone_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_zone_proto_rawDesc), len(file_proto_mcns_v1_zone_proto_rawDesc)), + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_mcns_v1_zone_proto_goTypes, + DependencyIndexes: file_proto_mcns_v1_zone_proto_depIdxs, + MessageInfos: file_proto_mcns_v1_zone_proto_msgTypes, + }.Build() + File_proto_mcns_v1_zone_proto = out.File + file_proto_mcns_v1_zone_proto_goTypes = nil + file_proto_mcns_v1_zone_proto_depIdxs = nil +} diff --git a/gen/mcns/v1/zone_grpc.pb.go b/gen/mcns/v1/zone_grpc.pb.go new file mode 100644 index 0000000..cec965e --- /dev/null +++ b/gen/mcns/v1/zone_grpc.pb.go @@ -0,0 +1,273 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v6.32.1 +// source: proto/mcns/v1/zone.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ZoneService_ListZones_FullMethodName = "/mcns.v1.ZoneService/ListZones" + ZoneService_CreateZone_FullMethodName = "/mcns.v1.ZoneService/CreateZone" + ZoneService_GetZone_FullMethodName = "/mcns.v1.ZoneService/GetZone" + ZoneService_UpdateZone_FullMethodName = "/mcns.v1.ZoneService/UpdateZone" + ZoneService_DeleteZone_FullMethodName = "/mcns.v1.ZoneService/DeleteZone" +) + +// ZoneServiceClient is the client API for ZoneService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ZoneServiceClient interface { + ListZones(ctx context.Context, in *ListZonesRequest, opts ...grpc.CallOption) (*ListZonesResponse, error) + CreateZone(ctx context.Context, in *CreateZoneRequest, opts ...grpc.CallOption) (*Zone, error) + GetZone(ctx context.Context, in *GetZoneRequest, opts ...grpc.CallOption) (*Zone, error) + UpdateZone(ctx context.Context, in *UpdateZoneRequest, opts ...grpc.CallOption) (*Zone, error) + DeleteZone(ctx context.Context, in *DeleteZoneRequest, opts ...grpc.CallOption) (*DeleteZoneResponse, error) +} + +type zoneServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewZoneServiceClient(cc grpc.ClientConnInterface) ZoneServiceClient { + return &zoneServiceClient{cc} +} + +func (c *zoneServiceClient) ListZones(ctx context.Context, in *ListZonesRequest, opts ...grpc.CallOption) (*ListZonesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListZonesResponse) + err := c.cc.Invoke(ctx, ZoneService_ListZones_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *zoneServiceClient) CreateZone(ctx context.Context, in *CreateZoneRequest, opts ...grpc.CallOption) (*Zone, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Zone) + err := c.cc.Invoke(ctx, ZoneService_CreateZone_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *zoneServiceClient) GetZone(ctx context.Context, in *GetZoneRequest, opts ...grpc.CallOption) (*Zone, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Zone) + err := c.cc.Invoke(ctx, ZoneService_GetZone_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *zoneServiceClient) UpdateZone(ctx context.Context, in *UpdateZoneRequest, opts ...grpc.CallOption) (*Zone, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Zone) + err := c.cc.Invoke(ctx, ZoneService_UpdateZone_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *zoneServiceClient) DeleteZone(ctx context.Context, in *DeleteZoneRequest, opts ...grpc.CallOption) (*DeleteZoneResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteZoneResponse) + err := c.cc.Invoke(ctx, ZoneService_DeleteZone_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ZoneServiceServer is the server API for ZoneService service. +// All implementations must embed UnimplementedZoneServiceServer +// for forward compatibility. +type ZoneServiceServer interface { + ListZones(context.Context, *ListZonesRequest) (*ListZonesResponse, error) + CreateZone(context.Context, *CreateZoneRequest) (*Zone, error) + GetZone(context.Context, *GetZoneRequest) (*Zone, error) + UpdateZone(context.Context, *UpdateZoneRequest) (*Zone, error) + DeleteZone(context.Context, *DeleteZoneRequest) (*DeleteZoneResponse, error) + mustEmbedUnimplementedZoneServiceServer() +} + +// UnimplementedZoneServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedZoneServiceServer struct{} + +func (UnimplementedZoneServiceServer) ListZones(context.Context, *ListZonesRequest) (*ListZonesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListZones not implemented") +} +func (UnimplementedZoneServiceServer) CreateZone(context.Context, *CreateZoneRequest) (*Zone, error) { + return nil, status.Error(codes.Unimplemented, "method CreateZone not implemented") +} +func (UnimplementedZoneServiceServer) GetZone(context.Context, *GetZoneRequest) (*Zone, error) { + return nil, status.Error(codes.Unimplemented, "method GetZone not implemented") +} +func (UnimplementedZoneServiceServer) UpdateZone(context.Context, *UpdateZoneRequest) (*Zone, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateZone not implemented") +} +func (UnimplementedZoneServiceServer) DeleteZone(context.Context, *DeleteZoneRequest) (*DeleteZoneResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteZone not implemented") +} +func (UnimplementedZoneServiceServer) mustEmbedUnimplementedZoneServiceServer() {} +func (UnimplementedZoneServiceServer) testEmbeddedByValue() {} + +// UnsafeZoneServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ZoneServiceServer will +// result in compilation errors. +type UnsafeZoneServiceServer interface { + mustEmbedUnimplementedZoneServiceServer() +} + +func RegisterZoneServiceServer(s grpc.ServiceRegistrar, srv ZoneServiceServer) { + // If the following call panics, it indicates UnimplementedZoneServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ZoneService_ServiceDesc, srv) +} + +func _ZoneService_ListZones_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListZonesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ZoneServiceServer).ListZones(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ZoneService_ListZones_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ZoneServiceServer).ListZones(ctx, req.(*ListZonesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ZoneService_CreateZone_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateZoneRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ZoneServiceServer).CreateZone(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ZoneService_CreateZone_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ZoneServiceServer).CreateZone(ctx, req.(*CreateZoneRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ZoneService_GetZone_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetZoneRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ZoneServiceServer).GetZone(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ZoneService_GetZone_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ZoneServiceServer).GetZone(ctx, req.(*GetZoneRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ZoneService_UpdateZone_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateZoneRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ZoneServiceServer).UpdateZone(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ZoneService_UpdateZone_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ZoneServiceServer).UpdateZone(ctx, req.(*UpdateZoneRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ZoneService_DeleteZone_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteZoneRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ZoneServiceServer).DeleteZone(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ZoneService_DeleteZone_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ZoneServiceServer).DeleteZone(ctx, req.(*DeleteZoneRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ZoneService_ServiceDesc is the grpc.ServiceDesc for ZoneService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ZoneService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "mcns.v1.ZoneService", + HandlerType: (*ZoneServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListZones", + Handler: _ZoneService_ListZones_Handler, + }, + { + MethodName: "CreateZone", + Handler: _ZoneService_CreateZone_Handler, + }, + { + MethodName: "GetZone", + Handler: _ZoneService_GetZone_Handler, + }, + { + MethodName: "UpdateZone", + Handler: _ZoneService_UpdateZone_Handler, + }, + { + MethodName: "DeleteZone", + Handler: _ZoneService_DeleteZone_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/mcns/v1/zone.proto", +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dcd32e5 --- /dev/null +++ b/go.mod @@ -0,0 +1,34 @@ +module git.wntrmute.dev/kyle/mcns + +go 1.25.7 + +require ( + git.wntrmute.dev/kyle/mcdsl v1.0.0 + github.com/go-chi/chi/v5 v5.2.5 + github.com/miekg/dns v1.1.66 + github.com/spf13/cobra v1.10.2 + google.golang.org/grpc v1.79.3 + google.golang.org/protobuf v1.36.11 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pelletier/go-toml/v2 v2.3.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/spf13/pflag v1.0.9 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/tools v0.42.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + modernc.org/libc v1.70.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.47.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ea04282 --- /dev/null +++ b/go.sum @@ -0,0 +1,103 @@ +git.wntrmute.dev/kyle/mcdsl v1.0.0 h1:YB7dx4gdNYKKcVySpL6UkwHqdCJ9Nl1yS0+eHk0hNtk= +git.wntrmute.dev/kyle/mcdsl v1.0.0/go.mod h1:wo0tGfUAxci3XnOe4/rFmR0RjUElKdYUazc+Np986sg= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= +github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= +modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..f04da0e --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,49 @@ +package config + +import ( + "fmt" + + mcdslconfig "git.wntrmute.dev/kyle/mcdsl/config" +) + +// Config is the top-level MCNS configuration. +type Config struct { + mcdslconfig.Base + DNS DNSConfig `toml:"dns"` +} + +// DNSConfig holds the DNS server settings. +type DNSConfig struct { + ListenAddr string `toml:"listen_addr"` + Upstreams []string `toml:"upstreams"` +} + +// Load reads a TOML config file, applies environment variable overrides +// (MCNS_ prefix), sets defaults, and validates required fields. +func Load(path string) (*Config, error) { + cfg, err := mcdslconfig.Load[Config](path, "MCNS") + if err != nil { + return nil, err + } + + // Apply DNS defaults. + if cfg.DNS.ListenAddr == "" { + cfg.DNS.ListenAddr = ":53" + } + if len(cfg.DNS.Upstreams) == 0 { + cfg.DNS.Upstreams = []string{"1.1.1.1:53", "8.8.8.8:53"} + } + + return cfg, nil +} + +// Validate implements the mcdsl config.Validator interface. +func (c *Config) Validate() error { + if c.Database.Path == "" { + return fmt.Errorf("database.path is required") + } + if c.MCIAS.ServerURL == "" { + return fmt.Errorf("mcias.server_url is required") + } + return nil +} diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..d6a791b --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,23 @@ +package db + +import ( + "database/sql" + "fmt" + + mcdsldb "git.wntrmute.dev/kyle/mcdsl/db" +) + +// DB wraps a SQLite database connection. +type DB struct { + *sql.DB +} + +// Open opens (or creates) a SQLite database at the given path with the +// standard Metacircular pragmas: WAL mode, foreign keys, busy timeout. +func Open(path string) (*DB, error) { + sqlDB, err := mcdsldb.Open(path) + if err != nil { + return nil, fmt.Errorf("db: %w", err) + } + return &DB{sqlDB}, nil +} diff --git a/internal/db/migrate.go b/internal/db/migrate.go new file mode 100644 index 0000000..87a31a7 --- /dev/null +++ b/internal/db/migrate.go @@ -0,0 +1,46 @@ +package db + +import ( + mcdsldb "git.wntrmute.dev/kyle/mcdsl/db" +) + +// Migrations is the ordered list of MCNS schema migrations. +var Migrations = []mcdsldb.Migration{ + { + Version: 1, + Name: "zones and records", + SQL: ` +CREATE TABLE IF NOT EXISTS zones ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + primary_ns TEXT NOT NULL, + admin_email TEXT NOT NULL, + refresh INTEGER NOT NULL DEFAULT 3600, + retry INTEGER NOT NULL DEFAULT 600, + expire INTEGER NOT NULL DEFAULT 86400, + minimum_ttl INTEGER NOT NULL DEFAULT 300, + serial INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) +); + +CREATE TABLE IF NOT EXISTS records ( + id INTEGER PRIMARY KEY, + zone_id INTEGER NOT NULL REFERENCES zones(id) ON DELETE CASCADE, + name TEXT NOT NULL, + type TEXT NOT NULL CHECK (type IN ('A', 'AAAA', 'CNAME')), + value TEXT NOT NULL, + ttl INTEGER NOT NULL DEFAULT 300, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + UNIQUE(zone_id, name, type, value) +); + +CREATE INDEX IF NOT EXISTS idx_records_zone_name ON records(zone_id, name);`, + }, +} + +// Migrate applies all pending migrations. +func (d *DB) Migrate() error { + return mcdsldb.Migrate(d.DB, Migrations) +} diff --git a/internal/db/records.go b/internal/db/records.go new file mode 100644 index 0000000..432bba9 --- /dev/null +++ b/internal/db/records.go @@ -0,0 +1,308 @@ +package db + +import ( + "database/sql" + "errors" + "fmt" + "net" + "strings" + "time" +) + +// Record represents a DNS record stored in the database. +type Record struct { + ID int64 + ZoneID int64 + ZoneName string + Name string + Type string + Value string + TTL int + CreatedAt string + UpdatedAt string +} + +// ListRecords returns records for a zone, optionally filtered by name and type. +func (d *DB) ListRecords(zoneName, name, recordType string) ([]Record, error) { + zoneName = strings.ToLower(strings.TrimSuffix(zoneName, ".")) + + zone, err := d.GetZone(zoneName) + if err != nil { + return nil, err + } + + query := `SELECT r.id, r.zone_id, z.name, r.name, r.type, r.value, r.ttl, r.created_at, r.updated_at + FROM records r JOIN zones z ON r.zone_id = z.id WHERE r.zone_id = ?` + args := []any{zone.ID} + + if name != "" { + query += ` AND r.name = ?` + args = append(args, strings.ToLower(name)) + } + if recordType != "" { + query += ` AND r.type = ?` + args = append(args, strings.ToUpper(recordType)) + } + + query += ` ORDER BY r.name, r.type, r.value` + + rows, err := d.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("list records: %w", err) + } + defer rows.Close() + + var records []Record + for rows.Next() { + var r Record + if err := rows.Scan(&r.ID, &r.ZoneID, &r.ZoneName, &r.Name, &r.Type, &r.Value, &r.TTL, &r.CreatedAt, &r.UpdatedAt); err != nil { + return nil, fmt.Errorf("scan record: %w", err) + } + records = append(records, r) + } + return records, rows.Err() +} + +// LookupRecords returns records matching a name and type within a zone. +// Used by the DNS handler for query resolution. +func (d *DB) LookupRecords(zoneName, name, recordType string) ([]Record, error) { + zoneName = strings.ToLower(strings.TrimSuffix(zoneName, ".")) + name = strings.ToLower(name) + + rows, err := d.Query(`SELECT r.id, r.zone_id, z.name, r.name, r.type, r.value, r.ttl, r.created_at, r.updated_at + FROM records r JOIN zones z ON r.zone_id = z.id + WHERE z.name = ? AND r.name = ? AND r.type = ?`, zoneName, name, strings.ToUpper(recordType)) + if err != nil { + return nil, fmt.Errorf("lookup records: %w", err) + } + defer rows.Close() + + var records []Record + for rows.Next() { + var r Record + if err := rows.Scan(&r.ID, &r.ZoneID, &r.ZoneName, &r.Name, &r.Type, &r.Value, &r.TTL, &r.CreatedAt, &r.UpdatedAt); err != nil { + return nil, fmt.Errorf("scan record: %w", err) + } + records = append(records, r) + } + return records, rows.Err() +} + +// LookupCNAME returns CNAME records for a name within a zone. +func (d *DB) LookupCNAME(zoneName, name string) ([]Record, error) { + return d.LookupRecords(zoneName, name, "CNAME") +} + +// HasRecordsForName checks if any records of the given types exist for a name. +func (d *DB) HasRecordsForName(tx *sql.Tx, zoneID int64, name string, types []string) (bool, error) { + placeholders := make([]string, len(types)) + args := []any{zoneID, strings.ToLower(name)} + for i, t := range types { + placeholders[i] = "?" + args = append(args, strings.ToUpper(t)) + } + query := fmt.Sprintf(`SELECT COUNT(*) FROM records WHERE zone_id = ? AND name = ? AND type IN (%s)`, strings.Join(placeholders, ",")) + + var count int + if err := tx.QueryRow(query, args...).Scan(&count); err != nil { + return false, err + } + return count > 0, nil +} + +// GetRecord returns a record by ID. +func (d *DB) GetRecord(id int64) (*Record, error) { + var r Record + err := d.QueryRow(`SELECT r.id, r.zone_id, z.name, r.name, r.type, r.value, r.ttl, r.created_at, r.updated_at + FROM records r JOIN zones z ON r.zone_id = z.id WHERE r.id = ?`, id). + Scan(&r.ID, &r.ZoneID, &r.ZoneName, &r.Name, &r.Type, &r.Value, &r.TTL, &r.CreatedAt, &r.UpdatedAt) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("get record: %w", err) + } + return &r, nil +} + +// CreateRecord inserts a new record, enforcing CNAME exclusivity and +// value validation. Bumps the zone serial within the same transaction. +func (d *DB) CreateRecord(zoneName, name, recordType, value string, ttl int) (*Record, error) { + zoneName = strings.ToLower(strings.TrimSuffix(zoneName, ".")) + name = strings.ToLower(name) + recordType = strings.ToUpper(recordType) + + if err := validateRecordValue(recordType, value); err != nil { + return nil, err + } + + zone, err := d.GetZone(zoneName) + if err != nil { + return nil, err + } + + if ttl <= 0 { + ttl = 300 + } + + tx, err := d.Begin() + if err != nil { + return nil, fmt.Errorf("begin tx: %w", err) + } + defer func() { _ = tx.Rollback() }() + + // Enforce CNAME exclusivity. + if recordType == "CNAME" { + hasAddr, err := d.HasRecordsForName(tx, zone.ID, name, []string{"A", "AAAA"}) + if err != nil { + return nil, fmt.Errorf("check cname exclusivity: %w", err) + } + if hasAddr { + return nil, fmt.Errorf("%w: CNAME record conflicts with existing A/AAAA record for %q", ErrConflict, name) + } + } else if recordType == "A" || recordType == "AAAA" { + hasCNAME, err := d.HasRecordsForName(tx, zone.ID, name, []string{"CNAME"}) + if err != nil { + return nil, fmt.Errorf("check cname exclusivity: %w", err) + } + if hasCNAME { + return nil, fmt.Errorf("%w: A/AAAA record conflicts with existing CNAME record for %q", ErrConflict, name) + } + } + + res, err := tx.Exec(`INSERT INTO records (zone_id, name, type, value, ttl) VALUES (?, ?, ?, ?, ?)`, + zone.ID, name, recordType, value, ttl) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint") { + return nil, fmt.Errorf("%w: record already exists", ErrConflict) + } + return nil, fmt.Errorf("insert record: %w", err) + } + + recordID, err := res.LastInsertId() + if err != nil { + return nil, fmt.Errorf("last insert id: %w", err) + } + + if err := d.BumpSerial(tx, zone.ID); err != nil { + return nil, fmt.Errorf("bump serial: %w", err) + } + + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("commit: %w", err) + } + + return d.GetRecord(recordID) +} + +// UpdateRecord updates an existing record's fields and bumps the zone serial. +func (d *DB) UpdateRecord(id int64, name, recordType, value string, ttl int) (*Record, error) { + name = strings.ToLower(name) + recordType = strings.ToUpper(recordType) + + if err := validateRecordValue(recordType, value); err != nil { + return nil, err + } + + existing, err := d.GetRecord(id) + if err != nil { + return nil, err + } + + if ttl <= 0 { + ttl = 300 + } + + tx, err := d.Begin() + if err != nil { + return nil, fmt.Errorf("begin tx: %w", err) + } + defer func() { _ = tx.Rollback() }() + + // Enforce CNAME exclusivity for the new type/name combo. + if recordType == "CNAME" { + hasAddr, err := d.HasRecordsForName(tx, existing.ZoneID, name, []string{"A", "AAAA"}) + if err != nil { + return nil, fmt.Errorf("check cname exclusivity: %w", err) + } + if hasAddr { + return nil, fmt.Errorf("%w: CNAME record conflicts with existing A/AAAA record for %q", ErrConflict, name) + } + } else if recordType == "A" || recordType == "AAAA" { + hasCNAME, err := d.HasRecordsForName(tx, existing.ZoneID, name, []string{"CNAME"}) + if err != nil { + return nil, fmt.Errorf("check cname exclusivity: %w", err) + } + if hasCNAME { + return nil, fmt.Errorf("%w: A/AAAA record conflicts with existing CNAME record for %q", ErrConflict, name) + } + } + + now := time.Now().UTC().Format("2006-01-02T15:04:05Z") + _, err = tx.Exec(`UPDATE records SET name = ?, type = ?, value = ?, ttl = ?, updated_at = ? WHERE id = ?`, + name, recordType, value, ttl, now, id) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint") { + return nil, fmt.Errorf("%w: record already exists", ErrConflict) + } + return nil, fmt.Errorf("update record: %w", err) + } + + if err := d.BumpSerial(tx, existing.ZoneID); err != nil { + return nil, fmt.Errorf("bump serial: %w", err) + } + + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("commit: %w", err) + } + + return d.GetRecord(id) +} + +// DeleteRecord deletes a record and bumps the zone serial. +func (d *DB) DeleteRecord(id int64) error { + existing, err := d.GetRecord(id) + if err != nil { + return err + } + + tx, err := d.Begin() + if err != nil { + return fmt.Errorf("begin tx: %w", err) + } + defer func() { _ = tx.Rollback() }() + + _, err = tx.Exec(`DELETE FROM records WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("delete record: %w", err) + } + + if err := d.BumpSerial(tx, existing.ZoneID); err != nil { + return fmt.Errorf("bump serial: %w", err) + } + + return tx.Commit() +} + +// validateRecordValue checks that a record value is valid for its type. +func validateRecordValue(recordType, value string) error { + switch recordType { + case "A": + ip := net.ParseIP(value) + if ip == nil || ip.To4() == nil { + return fmt.Errorf("invalid IPv4 address: %q", value) + } + case "AAAA": + ip := net.ParseIP(value) + if ip == nil || ip.To4() != nil { + return fmt.Errorf("invalid IPv6 address: %q", value) + } + case "CNAME": + if !strings.HasSuffix(value, ".") { + return fmt.Errorf("CNAME value must be a fully-qualified domain name ending with '.': %q", value) + } + default: + return fmt.Errorf("unsupported record type: %q", recordType) + } + return nil +} diff --git a/internal/db/records_test.go b/internal/db/records_test.go new file mode 100644 index 0000000..a5c7f1a --- /dev/null +++ b/internal/db/records_test.go @@ -0,0 +1,289 @@ +package db + +import ( + "errors" + "testing" +) + +func createTestZone(t *testing.T, db *DB) *Zone { + t.Helper() + zone, err := db.CreateZone("svc.mcp.metacircular.net", "ns.mcp.metacircular.net.", "admin.metacircular.net.", 3600, 600, 86400, 300) + if err != nil { + t.Fatalf("create zone: %v", err) + } + return zone +} + +func TestCreateRecordA(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + record, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300) + if err != nil { + t.Fatalf("create record: %v", err) + } + if record.Name != "metacrypt" { + t.Fatalf("got name %q, want %q", record.Name, "metacrypt") + } + if record.Type != "A" { + t.Fatalf("got type %q, want %q", record.Type, "A") + } + if record.Value != "192.168.88.181" { + t.Fatalf("got value %q, want %q", record.Value, "192.168.88.181") + } +} + +func TestCreateRecordAAAA(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + record, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "AAAA", "2001:db8::1", 300) + if err != nil { + t.Fatalf("create record: %v", err) + } + if record.Type != "AAAA" { + t.Fatalf("got type %q, want %q", record.Type, "AAAA") + } +} + +func TestCreateRecordCNAME(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + record, err := db.CreateRecord("svc.mcp.metacircular.net", "alias", "CNAME", "rift.mcp.metacircular.net.", 300) + if err != nil { + t.Fatalf("create record: %v", err) + } + if record.Type != "CNAME" { + t.Fatalf("got type %q, want %q", record.Type, "CNAME") + } +} + +func TestCreateRecordInvalidIP(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + _, err := db.CreateRecord("svc.mcp.metacircular.net", "bad", "A", "not-an-ip", 300) + if err == nil { + t.Fatal("expected error for invalid IPv4") + } +} + +func TestCreateRecordCNAMEExclusivity(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + // Create an A record first. + _, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300) + if err != nil { + t.Fatalf("create A record: %v", err) + } + + // Trying to add a CNAME for the same name should fail. + _, err = db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "CNAME", "rift.mcp.metacircular.net.", 300) + if !errors.Is(err, ErrConflict) { + t.Fatalf("expected ErrConflict, got %v", err) + } +} + +func TestCreateRecordCNAMEExclusivityReverse(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + // Create a CNAME record first. + _, err := db.CreateRecord("svc.mcp.metacircular.net", "alias", "CNAME", "rift.mcp.metacircular.net.", 300) + if err != nil { + t.Fatalf("create CNAME record: %v", err) + } + + // Trying to add an A record for the same name should fail. + _, err = db.CreateRecord("svc.mcp.metacircular.net", "alias", "A", "192.168.88.181", 300) + if !errors.Is(err, ErrConflict) { + t.Fatalf("expected ErrConflict, got %v", err) + } +} + +func TestCreateRecordBumpsSerial(t *testing.T) { + db := openTestDB(t) + zone := createTestZone(t, db) + originalSerial := zone.Serial + + _, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300) + if err != nil { + t.Fatalf("create record: %v", err) + } + + updated, err := db.GetZone("svc.mcp.metacircular.net") + if err != nil { + t.Fatalf("get zone: %v", err) + } + if updated.Serial <= originalSerial { + t.Fatalf("serial should have bumped: %d <= %d", updated.Serial, originalSerial) + } +} + +func TestListRecords(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + _, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300) + if err != nil { + t.Fatalf("create record 1: %v", err) + } + _, err = db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "100.95.252.120", 300) + if err != nil { + t.Fatalf("create record 2: %v", err) + } + _, err = db.CreateRecord("svc.mcp.metacircular.net", "mcr", "A", "192.168.88.181", 300) + if err != nil { + t.Fatalf("create record 3: %v", err) + } + + // List all records. + records, err := db.ListRecords("svc.mcp.metacircular.net", "", "") + if err != nil { + t.Fatalf("list records: %v", err) + } + if len(records) != 3 { + t.Fatalf("got %d records, want 3", len(records)) + } + + // Filter by name. + records, err = db.ListRecords("svc.mcp.metacircular.net", "metacrypt", "") + if err != nil { + t.Fatalf("list records by name: %v", err) + } + if len(records) != 2 { + t.Fatalf("got %d records, want 2", len(records)) + } + + // Filter by type. + records, err = db.ListRecords("svc.mcp.metacircular.net", "", "A") + if err != nil { + t.Fatalf("list records by type: %v", err) + } + if len(records) != 3 { + t.Fatalf("got %d records, want 3", len(records)) + } +} + +func TestUpdateRecord(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + record, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300) + if err != nil { + t.Fatalf("create record: %v", err) + } + + updated, err := db.UpdateRecord(record.ID, "metacrypt", "A", "10.0.0.1", 600) + if err != nil { + t.Fatalf("update record: %v", err) + } + if updated.Value != "10.0.0.1" { + t.Fatalf("got value %q, want %q", updated.Value, "10.0.0.1") + } + if updated.TTL != 600 { + t.Fatalf("got ttl %d, want 600", updated.TTL) + } +} + +func TestDeleteRecord(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + record, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300) + if err != nil { + t.Fatalf("create record: %v", err) + } + + if err := db.DeleteRecord(record.ID); err != nil { + t.Fatalf("delete record: %v", err) + } + + _, err = db.GetRecord(record.ID) + if !errors.Is(err, ErrNotFound) { + t.Fatalf("expected ErrNotFound after delete, got %v", err) + } +} + +func TestDeleteRecordBumpsSerial(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + record, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300) + if err != nil { + t.Fatalf("create record: %v", err) + } + + zone, err := db.GetZone("svc.mcp.metacircular.net") + if err != nil { + t.Fatalf("get zone: %v", err) + } + serialBefore := zone.Serial + + if err := db.DeleteRecord(record.ID); err != nil { + t.Fatalf("delete record: %v", err) + } + + zone, err = db.GetZone("svc.mcp.metacircular.net") + if err != nil { + t.Fatalf("get zone after delete: %v", err) + } + if zone.Serial <= serialBefore { + t.Fatalf("serial should have bumped: %d <= %d", zone.Serial, serialBefore) + } +} + +func TestLookupRecords(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + _, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300) + if err != nil { + t.Fatalf("create record: %v", err) + } + + records, err := db.LookupRecords("svc.mcp.metacircular.net", "metacrypt", "A") + if err != nil { + t.Fatalf("lookup records: %v", err) + } + if len(records) != 1 { + t.Fatalf("got %d records, want 1", len(records)) + } + if records[0].Value != "192.168.88.181" { + t.Fatalf("got value %q, want %q", records[0].Value, "192.168.88.181") + } +} + +func TestCreateRecordCNAMEMissingDot(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + _, err := db.CreateRecord("svc.mcp.metacircular.net", "alias", "CNAME", "rift.mcp.metacircular.net", 300) + if err == nil { + t.Fatal("expected error for CNAME without trailing dot") + } +} + +func TestMultipleARecords(t *testing.T) { + db := openTestDB(t) + createTestZone(t, db) + + _, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300) + if err != nil { + t.Fatalf("create first A record: %v", err) + } + _, err = db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "100.95.252.120", 300) + if err != nil { + t.Fatalf("create second A record: %v", err) + } + + records, err := db.LookupRecords("svc.mcp.metacircular.net", "metacrypt", "A") + if err != nil { + t.Fatalf("lookup records: %v", err) + } + if len(records) != 2 { + t.Fatalf("got %d records, want 2", len(records)) + } +} diff --git a/internal/db/zones.go b/internal/db/zones.go new file mode 100644 index 0000000..2272268 --- /dev/null +++ b/internal/db/zones.go @@ -0,0 +1,182 @@ +package db + +import ( + "database/sql" + "errors" + "fmt" + "strconv" + "strings" + "time" +) + +// Zone represents a DNS zone stored in the database. +type Zone struct { + ID int64 + Name string + PrimaryNS string + AdminEmail string + Refresh int + Retry int + Expire int + MinimumTTL int + Serial int64 + CreatedAt string + UpdatedAt string +} + +// ErrNotFound is returned when a requested resource does not exist. +var ErrNotFound = errors.New("not found") + +// ErrConflict is returned when a write conflicts with existing data. +var ErrConflict = errors.New("conflict") + +// ListZones returns all zones ordered by name. +func (d *DB) ListZones() ([]Zone, error) { + rows, err := d.Query(`SELECT id, name, primary_ns, admin_email, refresh, retry, expire, minimum_ttl, serial, created_at, updated_at FROM zones ORDER BY name`) + if err != nil { + return nil, fmt.Errorf("list zones: %w", err) + } + defer rows.Close() + + var zones []Zone + for rows.Next() { + var z Zone + if err := rows.Scan(&z.ID, &z.Name, &z.PrimaryNS, &z.AdminEmail, &z.Refresh, &z.Retry, &z.Expire, &z.MinimumTTL, &z.Serial, &z.CreatedAt, &z.UpdatedAt); err != nil { + return nil, fmt.Errorf("scan zone: %w", err) + } + zones = append(zones, z) + } + return zones, rows.Err() +} + +// GetZone returns a zone by name (case-insensitive). +func (d *DB) GetZone(name string) (*Zone, error) { + name = strings.ToLower(strings.TrimSuffix(name, ".")) + var z Zone + err := d.QueryRow(`SELECT id, name, primary_ns, admin_email, refresh, retry, expire, minimum_ttl, serial, created_at, updated_at FROM zones WHERE name = ?`, name). + Scan(&z.ID, &z.Name, &z.PrimaryNS, &z.AdminEmail, &z.Refresh, &z.Retry, &z.Expire, &z.MinimumTTL, &z.Serial, &z.CreatedAt, &z.UpdatedAt) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("get zone: %w", err) + } + return &z, nil +} + +// GetZoneByID returns a zone by ID. +func (d *DB) GetZoneByID(id int64) (*Zone, error) { + var z Zone + err := d.QueryRow(`SELECT id, name, primary_ns, admin_email, refresh, retry, expire, minimum_ttl, serial, created_at, updated_at FROM zones WHERE id = ?`, id). + Scan(&z.ID, &z.Name, &z.PrimaryNS, &z.AdminEmail, &z.Refresh, &z.Retry, &z.Expire, &z.MinimumTTL, &z.Serial, &z.CreatedAt, &z.UpdatedAt) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("get zone by id: %w", err) + } + return &z, nil +} + +// CreateZone inserts a new zone and returns it with the generated serial. +func (d *DB) CreateZone(name, primaryNS, adminEmail string, refresh, retry, expire, minimumTTL int) (*Zone, error) { + name = strings.ToLower(strings.TrimSuffix(name, ".")) + serial := nextSerial(0) + + res, err := d.Exec(`INSERT INTO zones (name, primary_ns, admin_email, refresh, retry, expire, minimum_ttl, serial) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + name, primaryNS, adminEmail, refresh, retry, expire, minimumTTL, serial) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint") { + return nil, fmt.Errorf("%w: zone %q already exists", ErrConflict, name) + } + return nil, fmt.Errorf("create zone: %w", err) + } + + id, err := res.LastInsertId() + if err != nil { + return nil, fmt.Errorf("create zone: last insert id: %w", err) + } + + return d.GetZoneByID(id) +} + +// UpdateZone updates a zone's SOA parameters and bumps the serial. +func (d *DB) UpdateZone(name, primaryNS, adminEmail string, refresh, retry, expire, minimumTTL int) (*Zone, error) { + name = strings.ToLower(strings.TrimSuffix(name, ".")) + + zone, err := d.GetZone(name) + if err != nil { + return nil, err + } + + serial := nextSerial(zone.Serial) + now := time.Now().UTC().Format("2006-01-02T15:04:05Z") + + _, err = d.Exec(`UPDATE zones SET primary_ns = ?, admin_email = ?, refresh = ?, retry = ?, expire = ?, minimum_ttl = ?, serial = ?, updated_at = ? WHERE id = ?`, + primaryNS, adminEmail, refresh, retry, expire, minimumTTL, serial, now, zone.ID) + if err != nil { + return nil, fmt.Errorf("update zone: %w", err) + } + + return d.GetZoneByID(zone.ID) +} + +// DeleteZone deletes a zone and all its records. +func (d *DB) DeleteZone(name string) error { + name = strings.ToLower(strings.TrimSuffix(name, ".")) + res, err := d.Exec(`DELETE FROM zones WHERE name = ?`, name) + if err != nil { + return fmt.Errorf("delete zone: %w", err) + } + n, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("delete zone: rows affected: %w", err) + } + if n == 0 { + return ErrNotFound + } + return nil +} + +// BumpSerial increments the serial for a zone within a transaction. +func (d *DB) BumpSerial(tx *sql.Tx, zoneID int64) error { + var current int64 + if err := tx.QueryRow(`SELECT serial FROM zones WHERE id = ?`, zoneID).Scan(¤t); err != nil { + return fmt.Errorf("read serial: %w", err) + } + serial := nextSerial(current) + now := time.Now().UTC().Format("2006-01-02T15:04:05Z") + _, err := tx.Exec(`UPDATE zones SET serial = ?, updated_at = ? WHERE id = ?`, serial, now, zoneID) + return err +} + +// ZoneNames returns all zone names for the DNS handler. +func (d *DB) ZoneNames() ([]string, error) { + rows, err := d.Query(`SELECT name FROM zones ORDER BY name`) + if err != nil { + return nil, err + } + defer rows.Close() + + var names []string + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, err + } + names = append(names, name) + } + return names, rows.Err() +} + +// nextSerial computes the next SOA serial in YYYYMMDDNN format. +func nextSerial(current int64) int64 { + today := time.Now().UTC() + datePrefix, _ := strconv.ParseInt(today.Format("20060102"), 10, 64) + datePrefix *= 100 // YYYYMMDD00 + + if current >= datePrefix { + return current + 1 + } + return datePrefix + 1 +} diff --git a/internal/db/zones_test.go b/internal/db/zones_test.go new file mode 100644 index 0000000..7a69546 --- /dev/null +++ b/internal/db/zones_test.go @@ -0,0 +1,167 @@ +package db + +import ( + "path/filepath" + "testing" +) + +func openTestDB(t *testing.T) *DB { + t.Helper() + dir := t.TempDir() + database, err := Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("open db: %v", err) + } + if err := database.Migrate(); err != nil { + t.Fatalf("migrate: %v", err) + } + t.Cleanup(func() { _ = database.Close() }) + return database +} + +func TestCreateZone(t *testing.T) { + db := openTestDB(t) + + zone, err := db.CreateZone("example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300) + if err != nil { + t.Fatalf("create zone: %v", err) + } + if zone.Name != "example.com" { + t.Fatalf("got name %q, want %q", zone.Name, "example.com") + } + if zone.Serial == 0 { + t.Fatal("serial should not be zero") + } + if zone.PrimaryNS != "ns1.example.com." { + t.Fatalf("got primary_ns %q, want %q", zone.PrimaryNS, "ns1.example.com.") + } +} + +func TestCreateZoneDuplicate(t *testing.T) { + db := openTestDB(t) + + _, err := db.CreateZone("example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300) + if err != nil { + t.Fatalf("create zone: %v", err) + } + + _, err = db.CreateZone("example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300) + if err == nil { + t.Fatal("expected error for duplicate zone") + } +} + +func TestCreateZoneNormalization(t *testing.T) { + db := openTestDB(t) + + zone, err := db.CreateZone("Example.COM.", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300) + if err != nil { + t.Fatalf("create zone: %v", err) + } + if zone.Name != "example.com" { + t.Fatalf("got name %q, want %q", zone.Name, "example.com") + } +} + +func TestListZones(t *testing.T) { + db := openTestDB(t) + + _, err := db.CreateZone("b.example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300) + if err != nil { + t.Fatalf("create zone b: %v", err) + } + _, err = db.CreateZone("a.example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300) + if err != nil { + t.Fatalf("create zone a: %v", err) + } + + zones, err := db.ListZones() + if err != nil { + t.Fatalf("list zones: %v", err) + } + if len(zones) != 2 { + t.Fatalf("got %d zones, want 2", len(zones)) + } + if zones[0].Name != "a.example.com" { + t.Fatalf("zones should be ordered by name, got %q first", zones[0].Name) + } +} + +func TestGetZone(t *testing.T) { + db := openTestDB(t) + + _, err := db.CreateZone("example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300) + if err != nil { + t.Fatalf("create zone: %v", err) + } + + zone, err := db.GetZone("example.com") + if err != nil { + t.Fatalf("get zone: %v", err) + } + if zone.Name != "example.com" { + t.Fatalf("got name %q, want %q", zone.Name, "example.com") + } + + _, err = db.GetZone("nonexistent.com") + if err != ErrNotFound { + t.Fatalf("expected ErrNotFound, got %v", err) + } +} + +func TestUpdateZone(t *testing.T) { + db := openTestDB(t) + + original, err := db.CreateZone("example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300) + if err != nil { + t.Fatalf("create zone: %v", err) + } + + updated, err := db.UpdateZone("example.com", "ns2.example.com.", "newadmin.example.com.", 7200, 1200, 172800, 600) + if err != nil { + t.Fatalf("update zone: %v", err) + } + + if updated.PrimaryNS != "ns2.example.com." { + t.Fatalf("got primary_ns %q, want %q", updated.PrimaryNS, "ns2.example.com.") + } + if updated.Serial <= original.Serial { + t.Fatalf("serial should have incremented: %d <= %d", updated.Serial, original.Serial) + } +} + +func TestDeleteZone(t *testing.T) { + db := openTestDB(t) + + _, err := db.CreateZone("example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300) + if err != nil { + t.Fatalf("create zone: %v", err) + } + + if err := db.DeleteZone("example.com"); err != nil { + t.Fatalf("delete zone: %v", err) + } + + _, err = db.GetZone("example.com") + if err != ErrNotFound { + t.Fatalf("expected ErrNotFound after delete, got %v", err) + } + + if err := db.DeleteZone("nonexistent.com"); err != ErrNotFound { + t.Fatalf("expected ErrNotFound for nonexistent zone, got %v", err) + } +} + +func TestNextSerial(t *testing.T) { + // A zero serial should produce a date-based serial. + s1 := nextSerial(0) + if s1 < 2026032600 { + t.Fatalf("serial %d seems too low", s1) + } + + // Incrementing should increase. + s2 := nextSerial(s1) + if s2 != s1+1 { + t.Fatalf("expected %d, got %d", s1+1, s2) + } +} diff --git a/internal/dns/cache.go b/internal/dns/cache.go new file mode 100644 index 0000000..0fb97e3 --- /dev/null +++ b/internal/dns/cache.go @@ -0,0 +1,67 @@ +package dns + +import ( + "sync" + "time" + + "github.com/miekg/dns" +) + +type cacheKey struct { + Name string + Qtype uint16 + Class uint16 +} + +type cacheEntry struct { + msg *dns.Msg + expiresAt time.Time +} + +// Cache is a thread-safe in-memory DNS response cache with TTL-based expiry. +type Cache struct { + mu sync.RWMutex + entries map[cacheKey]*cacheEntry +} + +// NewCache creates an empty DNS cache. +func NewCache() *Cache { + return &Cache{ + entries: make(map[cacheKey]*cacheEntry), + } +} + +// Get returns a cached response if it exists and has not expired. +func (c *Cache) Get(name string, qtype, class uint16) *dns.Msg { + c.mu.RLock() + defer c.mu.RUnlock() + + key := cacheKey{Name: name, Qtype: qtype, Class: class} + entry, ok := c.entries[key] + if !ok || time.Now().After(entry.expiresAt) { + return nil + } + return entry.msg +} + +// Set stores a DNS response in the cache with the given TTL. +func (c *Cache) Set(name string, qtype, class uint16, msg *dns.Msg, ttl time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + + key := cacheKey{Name: name, Qtype: qtype, Class: class} + c.entries[key] = &cacheEntry{ + msg: msg.Copy(), + expiresAt: time.Now().Add(ttl), + } + + // Lazy eviction: clean up expired entries if cache is growing. + if len(c.entries) > 1000 { + now := time.Now() + for k, v := range c.entries { + if now.After(v.expiresAt) { + delete(c.entries, k) + } + } + } +} diff --git a/internal/dns/cache_test.go b/internal/dns/cache_test.go new file mode 100644 index 0000000..66ef1be --- /dev/null +++ b/internal/dns/cache_test.go @@ -0,0 +1,81 @@ +package dns + +import ( + "testing" + "time" + + "github.com/miekg/dns" +) + +func TestCacheSetGet(t *testing.T) { + c := NewCache() + + msg := new(dns.Msg) + msg.SetQuestion("example.com.", dns.TypeA) + msg.Answer = append(msg.Answer, &dns.A{ + Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60}, + A: []byte{1, 2, 3, 4}, + }) + + c.Set("example.com.", dns.TypeA, dns.ClassINET, msg, 5*time.Second) + + cached := c.Get("example.com.", dns.TypeA, dns.ClassINET) + if cached == nil { + t.Fatal("expected cached response") + } + if len(cached.Answer) != 1 { + t.Fatalf("got %d answers, want 1", len(cached.Answer)) + } +} + +func TestCacheMiss(t *testing.T) { + c := NewCache() + + cached := c.Get("example.com.", dns.TypeA, dns.ClassINET) + if cached != nil { + t.Fatal("expected nil for cache miss") + } +} + +func TestCacheExpiry(t *testing.T) { + c := NewCache() + + msg := new(dns.Msg) + msg.SetQuestion("example.com.", dns.TypeA) + + c.Set("example.com.", dns.TypeA, dns.ClassINET, msg, 1*time.Millisecond) + time.Sleep(2 * time.Millisecond) + + cached := c.Get("example.com.", dns.TypeA, dns.ClassINET) + if cached != nil { + t.Fatal("expected nil for expired entry") + } +} + +func TestCacheDifferentTypes(t *testing.T) { + c := NewCache() + + msgA := new(dns.Msg) + msgA.SetQuestion("example.com.", dns.TypeA) + c.Set("example.com.", dns.TypeA, dns.ClassINET, msgA, 5*time.Second) + + msgAAAA := new(dns.Msg) + msgAAAA.SetQuestion("example.com.", dns.TypeAAAA) + c.Set("example.com.", dns.TypeAAAA, dns.ClassINET, msgAAAA, 5*time.Second) + + cachedA := c.Get("example.com.", dns.TypeA, dns.ClassINET) + if cachedA == nil { + t.Fatal("expected cached A response") + } + + cachedAAAA := c.Get("example.com.", dns.TypeAAAA, dns.ClassINET) + if cachedAAAA == nil { + t.Fatal("expected cached AAAA response") + } + + // Different type should not match. + cachedMX := c.Get("example.com.", dns.TypeMX, dns.ClassINET) + if cachedMX != nil { + t.Fatal("expected nil for uncached type") + } +} diff --git a/internal/dns/forwarder.go b/internal/dns/forwarder.go new file mode 100644 index 0000000..94816d8 --- /dev/null +++ b/internal/dns/forwarder.go @@ -0,0 +1,87 @@ +package dns + +import ( + "fmt" + "time" + + "github.com/miekg/dns" +) + +// Forwarder handles forwarding DNS queries to upstream resolvers. +type Forwarder struct { + upstreams []string + client *dns.Client + cache *Cache +} + +// NewForwarder creates a Forwarder with the given upstream addresses. +func NewForwarder(upstreams []string) *Forwarder { + return &Forwarder{ + upstreams: upstreams, + client: &dns.Client{ + Timeout: 2 * time.Second, + }, + cache: NewCache(), + } +} + +// Forward sends a query to upstream resolvers and returns the response. +// Responses are cached by (qname, qtype, qclass) with TTL-based expiry. +func (f *Forwarder) Forward(r *dns.Msg) (*dns.Msg, error) { + if len(r.Question) == 0 { + return nil, fmt.Errorf("empty question") + } + + q := r.Question[0] + + // Check cache. + if cached := f.cache.Get(q.Name, q.Qtype, q.Qclass); cached != nil { + return cached.Copy(), nil + } + + // Try each upstream in order. + var lastErr error + for _, upstream := range f.upstreams { + resp, _, err := f.client.Exchange(r, upstream) + if err != nil { + lastErr = err + continue + } + + // Don't cache SERVFAIL or REFUSED. + if resp.Rcode != dns.RcodeServerFailure && resp.Rcode != dns.RcodeRefused { + ttl := minTTL(resp) + if ttl > 300 { + ttl = 300 + } + if ttl > 0 { + f.cache.Set(q.Name, q.Qtype, q.Qclass, resp, time.Duration(ttl)*time.Second) + } + } + + return resp, nil + } + + return nil, fmt.Errorf("all upstreams failed: %w", lastErr) +} + +// minTTL returns the minimum TTL from all resource records in a response. +func minTTL(msg *dns.Msg) uint32 { + var min uint32 + first := true + + for _, sections := range [][]dns.RR{msg.Answer, msg.Ns, msg.Extra} { + for _, rr := range sections { + ttl := rr.Header().Ttl + if first || ttl < min { + min = ttl + first = false + } + } + } + + if first { + return 60 // No records; default to 60s. + } + return min +} diff --git a/internal/dns/server.go b/internal/dns/server.go new file mode 100644 index 0000000..354b9b9 --- /dev/null +++ b/internal/dns/server.go @@ -0,0 +1,280 @@ +// Package dns implements the authoritative DNS server for MCNS. +// It serves records from SQLite for authoritative zones and forwards +// all other queries to configured upstream resolvers. +package dns + +import ( + "log/slog" + "net" + "strings" + + "github.com/miekg/dns" + + "git.wntrmute.dev/kyle/mcns/internal/db" +) + +// Server is the MCNS DNS server. It listens on both UDP and TCP. +type Server struct { + db *db.DB + forwarder *Forwarder + logger *slog.Logger + udp *dns.Server + tcp *dns.Server +} + +// New creates a DNS server that serves records from the database and +// forwards non-authoritative queries to the given upstreams. +func New(database *db.DB, upstreams []string, logger *slog.Logger) *Server { + s := &Server{ + db: database, + forwarder: NewForwarder(upstreams), + logger: logger, + } + + mux := dns.NewServeMux() + mux.HandleFunc(".", s.handleQuery) + + s.udp = &dns.Server{Handler: mux, Net: "udp"} + s.tcp = &dns.Server{Handler: mux, Net: "tcp"} + + return s +} + +// ListenAndServe starts the DNS server on the given address for both +// UDP and TCP. It blocks until Shutdown is called. +func (s *Server) ListenAndServe(addr string) error { + s.udp.Addr = addr + s.tcp.Addr = addr + + errCh := make(chan error, 2) + + go func() { + s.logger.Info("dns server listening", "addr", addr, "proto", "udp") + errCh <- s.udp.ListenAndServe() + }() + go func() { + s.logger.Info("dns server listening", "addr", addr, "proto", "tcp") + errCh <- s.tcp.ListenAndServe() + }() + + return <-errCh +} + +// Shutdown gracefully stops the DNS server. +func (s *Server) Shutdown() { + _ = s.udp.Shutdown() + _ = s.tcp.Shutdown() +} + +// handleQuery is the main DNS query handler. It checks if the query +// falls within an authoritative zone and either serves from the database +// or forwards to upstream. +func (s *Server) handleQuery(w dns.ResponseWriter, r *dns.Msg) { + if len(r.Question) == 0 { + s.writeResponse(w, r, dns.RcodeFormatError, nil, nil) + return + } + + q := r.Question[0] + qname := strings.ToLower(q.Name) + + // Find the authoritative zone for this query. + zone := s.findZone(qname) + if zone == nil { + // Not authoritative — forward to upstream. + s.forwardQuery(w, r) + return + } + + s.handleAuthoritativeQuery(w, r, zone, qname, q.Qtype) +} + +// findZone returns the best matching zone for the query name, or nil. +func (s *Server) findZone(qname string) *db.Zone { + // Walk up the domain labels to find the longest matching zone. + name := strings.TrimSuffix(qname, ".") + parts := strings.Split(name, ".") + + for i := range parts { + candidate := strings.Join(parts[i:], ".") + zone, err := s.db.GetZone(candidate) + if err == nil { + return zone + } + } + return nil +} + +// handleAuthoritativeQuery serves a query from the database. +func (s *Server) handleAuthoritativeQuery(w dns.ResponseWriter, r *dns.Msg, zone *db.Zone, qname string, qtype uint16) { + // Extract the record name relative to the zone. + zoneFQDN := zone.Name + "." + var relName string + if qname == zoneFQDN { + relName = "@" + } else { + relName = strings.TrimSuffix(qname, "."+zoneFQDN) + } + + // Handle SOA queries. + if qtype == dns.TypeSOA || relName == "@" && qtype == dns.TypeSOA { + soa := s.buildSOA(zone) + s.writeResponse(w, r, dns.RcodeSuccess, []dns.RR{soa}, nil) + return + } + + // Handle NS queries at the zone apex. + if qtype == dns.TypeNS && relName == "@" { + ns := &dns.NS{ + Hdr: dns.RR_Header{Name: zoneFQDN, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: uint32(zone.MinimumTTL)}, + Ns: zone.PrimaryNS, + } + s.writeResponse(w, r, dns.RcodeSuccess, []dns.RR{ns}, nil) + return + } + + // Look up the requested record type. + var answers []dns.RR + var lookupType string + + switch qtype { + case dns.TypeA: + lookupType = "A" + case dns.TypeAAAA: + lookupType = "AAAA" + case dns.TypeCNAME: + lookupType = "CNAME" + default: + // For unsupported types, check if the name exists at all. + // If it does, return empty answer. If not, NXDOMAIN. + exists, _ := s.nameExists(zone.Name, relName) + if exists { + s.writeResponse(w, r, dns.RcodeSuccess, nil, []dns.RR{s.buildSOA(zone)}) + } else { + s.writeResponse(w, r, dns.RcodeNameError, nil, []dns.RR{s.buildSOA(zone)}) + } + return + } + + records, err := s.db.LookupRecords(zone.Name, relName, lookupType) + if err != nil { + s.logger.Error("dns lookup failed", "zone", zone.Name, "name", relName, "type", lookupType, "error", err) + s.writeResponse(w, r, dns.RcodeServerFailure, nil, nil) + return + } + + // If no direct records, check for CNAME. + if len(records) == 0 && (qtype == dns.TypeA || qtype == dns.TypeAAAA) { + cnameRecords, err := s.db.LookupCNAME(zone.Name, relName) + if err == nil && len(cnameRecords) > 0 { + for _, rec := range cnameRecords { + answers = append(answers, &dns.CNAME{ + Hdr: dns.RR_Header{Name: qname, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: uint32(rec.TTL)}, + Target: rec.Value, + }) + } + s.writeResponse(w, r, dns.RcodeSuccess, answers, nil) + return + } + } + + if len(records) == 0 { + // Name might still exist with other record types. + exists, _ := s.nameExists(zone.Name, relName) + if exists { + // NODATA: name exists but no records of requested type. + s.writeResponse(w, r, dns.RcodeSuccess, nil, []dns.RR{s.buildSOA(zone)}) + } else { + // NXDOMAIN: name does not exist. + s.writeResponse(w, r, dns.RcodeNameError, nil, []dns.RR{s.buildSOA(zone)}) + } + return + } + + for _, rec := range records { + rr := s.recordToRR(qname, rec) + if rr != nil { + answers = append(answers, rr) + } + } + + s.writeResponse(w, r, dns.RcodeSuccess, answers, nil) +} + +// nameExists checks if any records exist for a name in a zone. +func (s *Server) nameExists(zoneName, name string) (bool, error) { + records, err := s.db.ListRecords(zoneName, name, "") + if err != nil { + return false, err + } + return len(records) > 0, nil +} + +// recordToRR converts a database Record to a dns.RR. +func (s *Server) recordToRR(qname string, rec db.Record) dns.RR { + hdr := dns.RR_Header{Name: qname, Class: dns.ClassINET, Ttl: uint32(rec.TTL)} + + switch rec.Type { + case "A": + hdr.Rrtype = dns.TypeA + return &dns.A{Hdr: hdr, A: parseIP(rec.Value)} + case "AAAA": + hdr.Rrtype = dns.TypeAAAA + return &dns.AAAA{Hdr: hdr, AAAA: parseIP(rec.Value)} + case "CNAME": + hdr.Rrtype = dns.TypeCNAME + return &dns.CNAME{Hdr: hdr, Target: rec.Value} + } + return nil +} + +// buildSOA constructs a SOA record for the given zone. +func (s *Server) buildSOA(zone *db.Zone) *dns.SOA { + return &dns.SOA{ + Hdr: dns.RR_Header{Name: zone.Name + ".", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: uint32(zone.MinimumTTL)}, + Ns: zone.PrimaryNS, + Mbox: zone.AdminEmail, + Serial: uint32(zone.Serial), + Refresh: uint32(zone.Refresh), + Retry: uint32(zone.Retry), + Expire: uint32(zone.Expire), + Minttl: uint32(zone.MinimumTTL), + } +} + +// writeResponse constructs and writes a DNS response. +func (s *Server) writeResponse(w dns.ResponseWriter, r *dns.Msg, rcode int, answer []dns.RR, ns []dns.RR) { + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative = true + m.Rcode = rcode + m.Answer = answer + m.Ns = ns + + if err := w.WriteMsg(m); err != nil { + s.logger.Error("dns write failed", "error", err) + } +} + +// forwardQuery forwards a DNS query to upstream resolvers. +func (s *Server) forwardQuery(w dns.ResponseWriter, r *dns.Msg) { + resp, err := s.forwarder.Forward(r) + if err != nil { + s.logger.Debug("dns forward failed", "error", err) + m := new(dns.Msg) + m.SetReply(r) + m.Rcode = dns.RcodeServerFailure + _ = w.WriteMsg(m) + return + } + + resp.Id = r.Id + if err := w.WriteMsg(resp); err != nil { + s.logger.Error("dns write failed", "error", err) + } +} + +// parseIP parses an IP address string into a net.IP. +func parseIP(s string) net.IP { + return net.ParseIP(s) +} diff --git a/internal/dns/server_test.go b/internal/dns/server_test.go new file mode 100644 index 0000000..feb9511 --- /dev/null +++ b/internal/dns/server_test.go @@ -0,0 +1,142 @@ +package dns + +import ( + "path/filepath" + "testing" + + "github.com/miekg/dns" + + "git.wntrmute.dev/kyle/mcns/internal/db" + "log/slog" +) + +func openTestDB(t *testing.T) *db.DB { + t.Helper() + dir := t.TempDir() + database, err := db.Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("open db: %v", err) + } + if err := database.Migrate(); err != nil { + t.Fatalf("migrate: %v", err) + } + t.Cleanup(func() { _ = database.Close() }) + return database +} + +func setupTestServer(t *testing.T) (*Server, *db.DB) { + t.Helper() + database := openTestDB(t) + logger := slog.Default() + + _, err := database.CreateZone("svc.mcp.metacircular.net", "ns.mcp.metacircular.net.", "admin.metacircular.net.", 3600, 600, 86400, 300) + if err != nil { + t.Fatalf("create zone: %v", err) + } + _, err = database.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300) + if err != nil { + t.Fatalf("create A record: %v", err) + } + _, err = database.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "100.95.252.120", 300) + if err != nil { + t.Fatalf("create A record 2: %v", err) + } + _, err = database.CreateRecord("svc.mcp.metacircular.net", "mcr", "AAAA", "2001:db8::1", 300) + if err != nil { + t.Fatalf("create AAAA record: %v", err) + } + _, err = database.CreateRecord("svc.mcp.metacircular.net", "alias", "CNAME", "metacrypt.svc.mcp.metacircular.net.", 300) + if err != nil { + t.Fatalf("create CNAME record: %v", err) + } + + srv := New(database, []string{"1.1.1.1:53"}, logger) + return srv, database +} + +func TestFindZone(t *testing.T) { + srv, _ := setupTestServer(t) + + zone := srv.findZone("metacrypt.svc.mcp.metacircular.net.") + if zone == nil { + t.Fatal("expected to find zone") + } + if zone.Name != "svc.mcp.metacircular.net" { + t.Fatalf("got zone %q, want %q", zone.Name, "svc.mcp.metacircular.net") + } + + zone = srv.findZone("nonexistent.com.") + if zone != nil { + t.Fatal("expected nil for nonexistent zone") + } +} + +func TestBuildSOA(t *testing.T) { + srv, database := setupTestServer(t) + zone, err := database.GetZone("svc.mcp.metacircular.net") + if err != nil { + t.Fatalf("get zone: %v", err) + } + + soa := srv.buildSOA(zone) + if soa.Ns != "ns.mcp.metacircular.net." { + t.Fatalf("got ns %q, want %q", soa.Ns, "ns.mcp.metacircular.net.") + } + if soa.Hdr.Name != "svc.mcp.metacircular.net." { + t.Fatalf("got name %q, want %q", soa.Hdr.Name, "svc.mcp.metacircular.net.") + } +} + +func TestRecordToRR_A(t *testing.T) { + srv, _ := setupTestServer(t) + + rec := db.Record{Name: "metacrypt", Type: "A", Value: "192.168.88.181", TTL: 300} + rr := srv.recordToRR("metacrypt.svc.mcp.metacircular.net.", rec) + if rr == nil { + t.Fatal("expected non-nil RR") + } + + a, ok := rr.(*dns.A) + if !ok { + t.Fatalf("expected *dns.A, got %T", rr) + } + if a.A.String() != "192.168.88.181" { + t.Fatalf("got IP %q, want %q", a.A.String(), "192.168.88.181") + } +} + +func TestRecordToRR_AAAA(t *testing.T) { + srv, _ := setupTestServer(t) + + rec := db.Record{Name: "mcr", Type: "AAAA", Value: "2001:db8::1", TTL: 300} + rr := srv.recordToRR("mcr.svc.mcp.metacircular.net.", rec) + if rr == nil { + t.Fatal("expected non-nil RR") + } + + aaaa, ok := rr.(*dns.AAAA) + if !ok { + t.Fatalf("expected *dns.AAAA, got %T", rr) + } + if aaaa.AAAA.String() != "2001:db8::1" { + t.Fatalf("got IP %q, want %q", aaaa.AAAA.String(), "2001:db8::1") + } +} + +func TestRecordToRR_CNAME(t *testing.T) { + srv, _ := setupTestServer(t) + + rec := db.Record{Name: "alias", Type: "CNAME", Value: "metacrypt.svc.mcp.metacircular.net.", TTL: 300} + rr := srv.recordToRR("alias.svc.mcp.metacircular.net.", rec) + if rr == nil { + t.Fatal("expected non-nil RR") + } + + cname, ok := rr.(*dns.CNAME) + if !ok { + t.Fatalf("expected *dns.CNAME, got %T", rr) + } + if cname.Target != "metacrypt.svc.mcp.metacircular.net." { + t.Fatalf("got target %q, want %q", cname.Target, "metacrypt.svc.mcp.metacircular.net.") + } +} diff --git a/internal/grpcserver/admin.go b/internal/grpcserver/admin.go new file mode 100644 index 0000000..e8f40a9 --- /dev/null +++ b/internal/grpcserver/admin.go @@ -0,0 +1,20 @@ +package grpcserver + +import ( + "context" + + pb "git.wntrmute.dev/kyle/mcns/gen/mcns/v1" + "git.wntrmute.dev/kyle/mcns/internal/db" +) + +type adminService struct { + pb.UnimplementedAdminServiceServer + db *db.DB +} + +func (s *adminService) Health(_ context.Context, _ *pb.HealthRequest) (*pb.HealthResponse, error) { + if err := s.db.Ping(); err != nil { + return &pb.HealthResponse{Status: "unhealthy"}, nil + } + return &pb.HealthResponse{Status: "ok"}, nil +} diff --git a/internal/grpcserver/auth_handler.go b/internal/grpcserver/auth_handler.go new file mode 100644 index 0000000..b1f858a --- /dev/null +++ b/internal/grpcserver/auth_handler.go @@ -0,0 +1,38 @@ +package grpcserver + +import ( + "context" + "errors" + + mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + pb "git.wntrmute.dev/kyle/mcns/gen/mcns/v1" +) + +type authService struct { + pb.UnimplementedAuthServiceServer + auth *mcdslauth.Authenticator +} + +func (s *authService) Login(_ context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) { + token, _, err := s.auth.Login(req.Username, req.Password, req.TotpCode) + if err != nil { + if errors.Is(err, mcdslauth.ErrInvalidCredentials) { + return nil, status.Error(codes.Unauthenticated, "invalid credentials") + } + if errors.Is(err, mcdslauth.ErrForbidden) { + return nil, status.Error(codes.PermissionDenied, "access denied by login policy") + } + return nil, status.Error(codes.Unavailable, "authentication service unavailable") + } + return &pb.LoginResponse{Token: token}, nil +} + +func (s *authService) Logout(_ context.Context, req *pb.LogoutRequest) (*pb.LogoutResponse, error) { + if err := s.auth.Logout(req.Token); err != nil { + return nil, status.Error(codes.Internal, "logout failed") + } + return &pb.LogoutResponse{}, nil +} diff --git a/internal/grpcserver/interceptors.go b/internal/grpcserver/interceptors.go new file mode 100644 index 0000000..38dd65c --- /dev/null +++ b/internal/grpcserver/interceptors.go @@ -0,0 +1,45 @@ +package grpcserver + +import ( + mcdslgrpc "git.wntrmute.dev/kyle/mcdsl/grpcserver" +) + +// methodMap builds the mcdsl grpcserver.MethodMap for MCNS. +// +// Adding a new RPC without adding it to the correct map is a security +// defect — the mcdsl auth interceptor denies unmapped methods by default. +func methodMap() mcdslgrpc.MethodMap { + return mcdslgrpc.MethodMap{ + Public: publicMethods(), + AuthRequired: authRequiredMethods(), + AdminRequired: adminRequiredMethods(), + } +} + +func publicMethods() map[string]bool { + return map[string]bool{ + "/mcns.v1.AdminService/Health": true, + "/mcns.v1.AuthService/Login": true, + } +} + +func authRequiredMethods() map[string]bool { + return map[string]bool{ + "/mcns.v1.AuthService/Logout": true, + "/mcns.v1.ZoneService/ListZones": true, + "/mcns.v1.ZoneService/GetZone": true, + "/mcns.v1.RecordService/ListRecords": true, + "/mcns.v1.RecordService/GetRecord": true, + } +} + +func adminRequiredMethods() map[string]bool { + return map[string]bool{ + "/mcns.v1.ZoneService/CreateZone": true, + "/mcns.v1.ZoneService/UpdateZone": true, + "/mcns.v1.ZoneService/DeleteZone": true, + "/mcns.v1.RecordService/CreateRecord": true, + "/mcns.v1.RecordService/UpdateRecord": true, + "/mcns.v1.RecordService/DeleteRecord": true, + } +} diff --git a/internal/grpcserver/records.go b/internal/grpcserver/records.go new file mode 100644 index 0000000..bdcec9f --- /dev/null +++ b/internal/grpcserver/records.go @@ -0,0 +1,110 @@ +package grpcserver + +import ( + "context" + "errors" + "time" + + "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 recordService struct { + pb.UnimplementedRecordServiceServer + db *db.DB +} + +func (s *recordService) ListRecords(_ context.Context, req *pb.ListRecordsRequest) (*pb.ListRecordsResponse, error) { + records, err := s.db.ListRecords(req.Zone, req.Name, req.Type) + 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 list records") + } + + resp := &pb.ListRecordsResponse{} + for _, r := range records { + resp.Records = append(resp.Records, recordToProto(r)) + } + return resp, nil +} + +func (s *recordService) GetRecord(_ context.Context, req *pb.GetRecordRequest) (*pb.Record, error) { + record, err := s.db.GetRecord(req.Id) + if errors.Is(err, db.ErrNotFound) { + return nil, status.Error(codes.NotFound, "record not found") + } + if err != nil { + return nil, status.Error(codes.Internal, "failed to get record") + } + return recordToProto(*record), nil +} + +func (s *recordService) CreateRecord(_ context.Context, req *pb.CreateRecordRequest) (*pb.Record, error) { + record, err := s.db.CreateRecord(req.Zone, req.Name, req.Type, req.Value, int(req.Ttl)) + if errors.Is(err, db.ErrNotFound) { + return nil, status.Error(codes.NotFound, "zone not found") + } + if errors.Is(err, db.ErrConflict) { + return nil, status.Error(codes.AlreadyExists, err.Error()) + } + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return recordToProto(*record), nil +} + +func (s *recordService) UpdateRecord(_ context.Context, req *pb.UpdateRecordRequest) (*pb.Record, error) { + record, err := s.db.UpdateRecord(req.Id, req.Name, req.Type, req.Value, int(req.Ttl)) + if errors.Is(err, db.ErrNotFound) { + return nil, status.Error(codes.NotFound, "record not found") + } + if errors.Is(err, db.ErrConflict) { + return nil, status.Error(codes.AlreadyExists, err.Error()) + } + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return recordToProto(*record), nil +} + +func (s *recordService) DeleteRecord(_ context.Context, req *pb.DeleteRecordRequest) (*pb.DeleteRecordResponse, error) { + err := s.db.DeleteRecord(req.Id) + if errors.Is(err, db.ErrNotFound) { + return nil, status.Error(codes.NotFound, "record not found") + } + if err != nil { + return nil, status.Error(codes.Internal, "failed to delete record") + } + return &pb.DeleteRecordResponse{}, nil +} + +func recordToProto(r db.Record) *pb.Record { + return &pb.Record{ + Id: r.ID, + Zone: r.ZoneName, + Name: r.Name, + Type: r.Type, + Value: r.Value, + Ttl: int32(r.TTL), + CreatedAt: parseRecordTimestamp(r.CreatedAt), + UpdatedAt: parseRecordTimestamp(r.UpdatedAt), + } +} + +func parseRecordTimestamp(s string) *timestamppb.Timestamp { + t, err := parseTime(s) + if err != nil { + return nil + } + return timestamppb.New(t) +} + +func parseTime(s string) (time.Time, error) { + return time.Parse("2006-01-02T15:04:05Z", s) +} diff --git a/internal/grpcserver/server.go b/internal/grpcserver/server.go new file mode 100644 index 0000000..a4da346 --- /dev/null +++ b/internal/grpcserver/server.go @@ -0,0 +1,50 @@ +package grpcserver + +import ( + "log/slog" + "net" + + mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth" + mcdslgrpc "git.wntrmute.dev/kyle/mcdsl/grpcserver" + + pb "git.wntrmute.dev/kyle/mcns/gen/mcns/v1" + "git.wntrmute.dev/kyle/mcns/internal/db" +) + +// Deps holds the dependencies injected into the gRPC server. +type Deps struct { + DB *db.DB + Authenticator *mcdslauth.Authenticator +} + +// Server wraps a mcdsl grpcserver.Server with MCNS-specific services. +type Server struct { + srv *mcdslgrpc.Server +} + +// New creates a configured gRPC server with MCNS services registered. +func New(certFile, keyFile string, deps Deps, logger *slog.Logger) (*Server, error) { + srv, err := mcdslgrpc.New(certFile, keyFile, deps.Authenticator, methodMap(), logger) + if err != nil { + return nil, err + } + + s := &Server{srv: srv} + + pb.RegisterAdminServiceServer(srv.GRPCServer, &adminService{db: deps.DB}) + pb.RegisterAuthServiceServer(srv.GRPCServer, &authService{auth: deps.Authenticator}) + pb.RegisterZoneServiceServer(srv.GRPCServer, &zoneService{db: deps.DB}) + pb.RegisterRecordServiceServer(srv.GRPCServer, &recordService{db: deps.DB}) + + return s, nil +} + +// Serve starts the gRPC server on the given listener. +func (s *Server) Serve(lis net.Listener) error { + return s.srv.GRPCServer.Serve(lis) +} + +// GracefulStop gracefully stops the gRPC server. +func (s *Server) GracefulStop() { + s.srv.Stop() +} diff --git a/internal/grpcserver/zones.go b/internal/grpcserver/zones.go new file mode 100644 index 0000000..f668a64 --- /dev/null +++ b/internal/grpcserver/zones.go @@ -0,0 +1,134 @@ +package grpcserver + +import ( + "context" + "errors" + + "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 +} + +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, zoneToProto(z)) + } + return resp, nil +} + +func (s *zoneService) GetZone(_ context.Context, req *pb.GetZoneRequest) (*pb.Zone, error) { + 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 zoneToProto(*zone), nil +} + +func (s *zoneService) CreateZone(_ context.Context, req *pb.CreateZoneRequest) (*pb.Zone, error) { + refresh := int(req.Refresh) + if refresh == 0 { + refresh = 3600 + } + retry := int(req.Retry) + if retry == 0 { + retry = 600 + } + expire := int(req.Expire) + if expire == 0 { + expire = 86400 + } + minTTL := int(req.MinimumTtl) + if minTTL == 0 { + minTTL = 300 + } + + 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 zoneToProto(*zone), nil +} + +func (s *zoneService) UpdateZone(_ context.Context, req *pb.UpdateZoneRequest) (*pb.Zone, error) { + refresh := int(req.Refresh) + if refresh == 0 { + refresh = 3600 + } + retry := int(req.Retry) + if retry == 0 { + retry = 600 + } + expire := int(req.Expire) + if expire == 0 { + expire = 86400 + } + minTTL := int(req.MinimumTtl) + if minTTL == 0 { + minTTL = 300 + } + + 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 zoneToProto(*zone), nil +} + +func (s *zoneService) DeleteZone(_ context.Context, req *pb.DeleteZoneRequest) (*pb.DeleteZoneResponse, error) { + 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 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: parseTimestamp(z.CreatedAt), + UpdatedAt: parseTimestamp(z.UpdatedAt), + } +} + +func parseTimestamp(s string) *timestamppb.Timestamp { + // SQLite stores as "2006-01-02T15:04:05Z". + t, err := parseTime(s) + if err != nil { + return nil + } + return timestamppb.New(t) +} diff --git a/internal/server/auth.go b/internal/server/auth.go new file mode 100644 index 0000000..b1aacab --- /dev/null +++ b/internal/server/auth.go @@ -0,0 +1,62 @@ +package server + +import ( + "encoding/json" + "errors" + "net/http" + + mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth" +) + +type loginRequest struct { + Username string `json:"username"` + Password string `json:"password"` + TOTPCode string `json:"totp_code"` +} + +type loginResponse struct { + Token string `json:"token"` +} + +func loginHandler(auth *mcdslauth.Authenticator) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req loginRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + + token, _, err := auth.Login(req.Username, req.Password, req.TOTPCode) + if err != nil { + if errors.Is(err, mcdslauth.ErrInvalidCredentials) { + writeError(w, http.StatusUnauthorized, "invalid credentials") + return + } + if errors.Is(err, mcdslauth.ErrForbidden) { + writeError(w, http.StatusForbidden, "access denied by login policy") + return + } + writeError(w, http.StatusServiceUnavailable, "authentication service unavailable") + return + } + + writeJSON(w, http.StatusOK, loginResponse{Token: token}) + } +} + +func logoutHandler(auth *mcdslauth.Authenticator) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + token := extractBearerToken(r) + if token == "" { + writeError(w, http.StatusUnauthorized, "authentication required") + return + } + + if err := auth.Logout(token); err != nil { + writeError(w, http.StatusInternalServerError, "logout failed") + return + } + + w.WriteHeader(http.StatusNoContent) + } +} diff --git a/internal/server/middleware.go b/internal/server/middleware.go new file mode 100644 index 0000000..890d31f --- /dev/null +++ b/internal/server/middleware.go @@ -0,0 +1,96 @@ +package server + +import ( + "context" + "log/slog" + "net/http" + "strings" + "time" + + mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth" +) + +type contextKey string + +const tokenInfoKey contextKey = "tokenInfo" + +// requireAuth returns middleware that validates Bearer tokens via MCIAS. +func requireAuth(auth *mcdslauth.Authenticator) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := extractBearerToken(r) + if token == "" { + writeError(w, http.StatusUnauthorized, "authentication required") + return + } + + info, err := auth.ValidateToken(token) + if err != nil { + writeError(w, http.StatusUnauthorized, "invalid or expired token") + return + } + + ctx := context.WithValue(r.Context(), tokenInfoKey, info) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +// requireAdmin is middleware that checks the caller has the admin role. +func requireAdmin(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + info := tokenInfoFromContext(r.Context()) + if info == nil || !info.IsAdmin { + writeError(w, http.StatusForbidden, "admin role required") + return + } + next.ServeHTTP(w, r) + }) +} + +// tokenInfoFromContext extracts the TokenInfo from the request context. +func tokenInfoFromContext(ctx context.Context) *mcdslauth.TokenInfo { + info, _ := ctx.Value(tokenInfoKey).(*mcdslauth.TokenInfo) + return info +} + +// extractBearerToken extracts a bearer token from the Authorization header. +func extractBearerToken(r *http.Request) string { + h := r.Header.Get("Authorization") + if h == "" { + return "" + } + const prefix = "Bearer " + if !strings.HasPrefix(h, prefix) { + return "" + } + return strings.TrimSpace(h[len(prefix):]) +} + +// loggingMiddleware logs HTTP requests. +func loggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + sw := &statusWriter{ResponseWriter: w, status: http.StatusOK} + next.ServeHTTP(sw, r) + logger.Info("http", + "method", r.Method, + "path", r.URL.Path, + "status", sw.status, + "duration", time.Since(start), + "remote", r.RemoteAddr, + ) + }) + } +} + +type statusWriter struct { + http.ResponseWriter + status int +} + +func (w *statusWriter) WriteHeader(code int) { + w.status = code + w.ResponseWriter.WriteHeader(code) +} diff --git a/internal/server/records.go b/internal/server/records.go new file mode 100644 index 0000000..d2967e6 --- /dev/null +++ b/internal/server/records.go @@ -0,0 +1,174 @@ +package server + +import ( + "encoding/json" + "errors" + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" + + "git.wntrmute.dev/kyle/mcns/internal/db" +) + +type createRecordRequest struct { + Name string `json:"name"` + Type string `json:"type"` + Value string `json:"value"` + TTL int `json:"ttl"` +} + +func listRecordsHandler(database *db.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + zoneName := chi.URLParam(r, "zone") + nameFilter := r.URL.Query().Get("name") + typeFilter := r.URL.Query().Get("type") + + records, err := database.ListRecords(zoneName, nameFilter, typeFilter) + if errors.Is(err, db.ErrNotFound) { + writeError(w, http.StatusNotFound, "zone not found") + return + } + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to list records") + return + } + if records == nil { + records = []db.Record{} + } + writeJSON(w, http.StatusOK, map[string]any{"records": records}) + } +} + +func getRecordHandler(database *db.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + writeError(w, http.StatusBadRequest, "invalid record ID") + return + } + + record, err := database.GetRecord(id) + if errors.Is(err, db.ErrNotFound) { + writeError(w, http.StatusNotFound, "record not found") + return + } + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to get record") + return + } + + writeJSON(w, http.StatusOK, record) + } +} + +func createRecordHandler(database *db.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + zoneName := chi.URLParam(r, "zone") + + var req createRecordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + + if req.Name == "" { + writeError(w, http.StatusBadRequest, "name is required") + return + } + if req.Type == "" { + writeError(w, http.StatusBadRequest, "type is required") + return + } + if req.Value == "" { + writeError(w, http.StatusBadRequest, "value is required") + return + } + + record, err := database.CreateRecord(zoneName, req.Name, req.Type, req.Value, req.TTL) + if errors.Is(err, db.ErrNotFound) { + writeError(w, http.StatusNotFound, "zone not found") + return + } + if errors.Is(err, db.ErrConflict) { + writeError(w, http.StatusConflict, err.Error()) + return + } + if err != nil { + writeError(w, http.StatusBadRequest, err.Error()) + return + } + + writeJSON(w, http.StatusCreated, record) + } +} + +func updateRecordHandler(database *db.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + writeError(w, http.StatusBadRequest, "invalid record ID") + return + } + + var req createRecordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + + if req.Name == "" { + writeError(w, http.StatusBadRequest, "name is required") + return + } + if req.Type == "" { + writeError(w, http.StatusBadRequest, "type is required") + return + } + if req.Value == "" { + writeError(w, http.StatusBadRequest, "value is required") + return + } + + record, err := database.UpdateRecord(id, req.Name, req.Type, req.Value, req.TTL) + if errors.Is(err, db.ErrNotFound) { + writeError(w, http.StatusNotFound, "record not found") + return + } + if errors.Is(err, db.ErrConflict) { + writeError(w, http.StatusConflict, err.Error()) + return + } + if err != nil { + writeError(w, http.StatusBadRequest, err.Error()) + return + } + + writeJSON(w, http.StatusOK, record) + } +} + +func deleteRecordHandler(database *db.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + writeError(w, http.StatusBadRequest, "invalid record ID") + return + } + + err = database.DeleteRecord(id) + if errors.Is(err, db.ErrNotFound) { + writeError(w, http.StatusNotFound, "record not found") + return + } + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to delete record") + return + } + + w.WriteHeader(http.StatusNoContent) + } +} diff --git a/internal/server/routes.go b/internal/server/routes.go new file mode 100644 index 0000000..e322624 --- /dev/null +++ b/internal/server/routes.go @@ -0,0 +1,71 @@ +package server + +import ( + "encoding/json" + "log/slog" + "net/http" + + "github.com/go-chi/chi/v5" + + mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth" + "git.wntrmute.dev/kyle/mcdsl/health" + + "git.wntrmute.dev/kyle/mcns/internal/db" +) + +// Deps holds dependencies injected into the REST handlers. +type Deps struct { + DB *db.DB + Auth *mcdslauth.Authenticator + Logger *slog.Logger +} + +// NewRouter builds the chi router with all MCNS REST endpoints. +func NewRouter(deps Deps) *chi.Mux { + r := chi.NewRouter() + r.Use(loggingMiddleware(deps.Logger)) + + // Public endpoints. + r.Post("/v1/auth/login", loginHandler(deps.Auth)) + r.Get("/v1/health", health.Handler(deps.DB.DB)) + + // Authenticated endpoints. + r.Group(func(r chi.Router) { + r.Use(requireAuth(deps.Auth)) + + r.Post("/v1/auth/logout", logoutHandler(deps.Auth)) + + // Zone endpoints — reads for all authenticated users, writes for admin. + r.Get("/v1/zones", listZonesHandler(deps.DB)) + r.Get("/v1/zones/{zone}", getZoneHandler(deps.DB)) + + // Admin-only zone mutations. + r.With(requireAdmin).Post("/v1/zones", createZoneHandler(deps.DB)) + r.With(requireAdmin).Put("/v1/zones/{zone}", updateZoneHandler(deps.DB)) + r.With(requireAdmin).Delete("/v1/zones/{zone}", deleteZoneHandler(deps.DB)) + + // Record endpoints — reads for all authenticated users, writes for admin. + r.Get("/v1/zones/{zone}/records", listRecordsHandler(deps.DB)) + r.Get("/v1/zones/{zone}/records/{id}", getRecordHandler(deps.DB)) + + // Admin-only record mutations. + r.With(requireAdmin).Post("/v1/zones/{zone}/records", createRecordHandler(deps.DB)) + r.With(requireAdmin).Put("/v1/zones/{zone}/records/{id}", updateRecordHandler(deps.DB)) + r.With(requireAdmin).Delete("/v1/zones/{zone}/records/{id}", deleteRecordHandler(deps.DB)) + }) + + return r +} + +// writeJSON writes a JSON response with the given status code. +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(v) +} + +// writeError writes a standard error response. +func writeError(w http.ResponseWriter, status int, message string) { + writeJSON(w, status, map[string]string{"error": message}) +} + diff --git a/internal/server/zones.go b/internal/server/zones.go new file mode 100644 index 0000000..559383b --- /dev/null +++ b/internal/server/zones.go @@ -0,0 +1,163 @@ +package server + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/go-chi/chi/v5" + + "git.wntrmute.dev/kyle/mcns/internal/db" +) + +type createZoneRequest struct { + Name string `json:"name"` + PrimaryNS string `json:"primary_ns"` + AdminEmail string `json:"admin_email"` + Refresh int `json:"refresh"` + Retry int `json:"retry"` + Expire int `json:"expire"` + MinimumTTL int `json:"minimum_ttl"` +} + +func listZonesHandler(database *db.DB) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + zones, err := database.ListZones() + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to list zones") + return + } + if zones == nil { + zones = []db.Zone{} + } + writeJSON(w, http.StatusOK, map[string]any{"zones": zones}) + } +} + +func getZoneHandler(database *db.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "zone") + zone, err := database.GetZone(name) + if errors.Is(err, db.ErrNotFound) { + writeError(w, http.StatusNotFound, "zone not found") + return + } + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to get zone") + return + } + writeJSON(w, http.StatusOK, zone) + } +} + +func createZoneHandler(database *db.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req createZoneRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + + if req.Name == "" { + writeError(w, http.StatusBadRequest, "name is required") + return + } + if req.PrimaryNS == "" { + writeError(w, http.StatusBadRequest, "primary_ns is required") + return + } + if req.AdminEmail == "" { + writeError(w, http.StatusBadRequest, "admin_email is required") + return + } + + // Apply defaults for SOA params. + if req.Refresh == 0 { + req.Refresh = 3600 + } + if req.Retry == 0 { + req.Retry = 600 + } + if req.Expire == 0 { + req.Expire = 86400 + } + if req.MinimumTTL == 0 { + req.MinimumTTL = 300 + } + + zone, err := database.CreateZone(req.Name, req.PrimaryNS, req.AdminEmail, req.Refresh, req.Retry, req.Expire, req.MinimumTTL) + if errors.Is(err, db.ErrConflict) { + writeError(w, http.StatusConflict, err.Error()) + return + } + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to create zone") + return + } + + writeJSON(w, http.StatusCreated, zone) + } +} + +func updateZoneHandler(database *db.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "zone") + + var req createZoneRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + + if req.PrimaryNS == "" { + writeError(w, http.StatusBadRequest, "primary_ns is required") + return + } + if req.AdminEmail == "" { + writeError(w, http.StatusBadRequest, "admin_email is required") + return + } + if req.Refresh == 0 { + req.Refresh = 3600 + } + if req.Retry == 0 { + req.Retry = 600 + } + if req.Expire == 0 { + req.Expire = 86400 + } + if req.MinimumTTL == 0 { + req.MinimumTTL = 300 + } + + zone, err := database.UpdateZone(name, req.PrimaryNS, req.AdminEmail, req.Refresh, req.Retry, req.Expire, req.MinimumTTL) + if errors.Is(err, db.ErrNotFound) { + writeError(w, http.StatusNotFound, "zone not found") + return + } + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to update zone") + return + } + + writeJSON(w, http.StatusOK, zone) + } +} + +func deleteZoneHandler(database *db.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "zone") + + err := database.DeleteZone(name) + if errors.Is(err, db.ErrNotFound) { + writeError(w, http.StatusNotFound, "zone not found") + return + } + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to delete zone") + return + } + + w.WriteHeader(http.StatusNoContent) + } +} diff --git a/proto/mcns/v1/admin.proto b/proto/mcns/v1/admin.proto new file mode 100644 index 0000000..9dcf262 --- /dev/null +++ b/proto/mcns/v1/admin.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package mcns.v1; + +option go_package = "git.wntrmute.dev/kyle/mcns/gen/mcns/v1"; + +service AdminService { + rpc Health(HealthRequest) returns (HealthResponse); +} + +message HealthRequest {} + +message HealthResponse { + string status = 1; +} diff --git a/proto/mcns/v1/auth.proto b/proto/mcns/v1/auth.proto new file mode 100644 index 0000000..25fe759 --- /dev/null +++ b/proto/mcns/v1/auth.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package mcns.v1; + +option go_package = "git.wntrmute.dev/kyle/mcns/gen/mcns/v1"; + +service AuthService { + rpc Login(LoginRequest) returns (LoginResponse); + rpc Logout(LogoutRequest) returns (LogoutResponse); +} + +message LoginRequest { + string username = 1; + string password = 2; + string totp_code = 3; +} + +message LoginResponse { + string token = 1; +} + +message LogoutRequest { + string token = 1; +} + +message LogoutResponse {} diff --git a/proto/mcns/v1/record.proto b/proto/mcns/v1/record.proto new file mode 100644 index 0000000..6f9b2ef --- /dev/null +++ b/proto/mcns/v1/record.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; + +package mcns.v1; + +option go_package = "git.wntrmute.dev/kyle/mcns/gen/mcns/v1"; + +import "google/protobuf/timestamp.proto"; + +service RecordService { + rpc ListRecords(ListRecordsRequest) returns (ListRecordsResponse); + rpc CreateRecord(CreateRecordRequest) returns (Record); + rpc GetRecord(GetRecordRequest) returns (Record); + rpc UpdateRecord(UpdateRecordRequest) returns (Record); + rpc DeleteRecord(DeleteRecordRequest) returns (DeleteRecordResponse); +} + +message Record { + int64 id = 1; + string zone = 2; + string name = 3; + string type = 4; + string value = 5; + int32 ttl = 6; + google.protobuf.Timestamp created_at = 7; + google.protobuf.Timestamp updated_at = 8; +} + +message ListRecordsRequest { + string zone = 1; + string name = 2; + string type = 3; +} + +message ListRecordsResponse { + repeated Record records = 1; +} + +message CreateRecordRequest { + string zone = 1; + string name = 2; + string type = 3; + string value = 4; + int32 ttl = 5; +} + +message GetRecordRequest { + int64 id = 1; +} + +message UpdateRecordRequest { + int64 id = 1; + string name = 2; + string type = 3; + string value = 4; + int32 ttl = 5; +} + +message DeleteRecordRequest { + int64 id = 1; +} + +message DeleteRecordResponse {} diff --git a/proto/mcns/v1/zone.proto b/proto/mcns/v1/zone.proto new file mode 100644 index 0000000..eb68aaf --- /dev/null +++ b/proto/mcns/v1/zone.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; + +package mcns.v1; + +option go_package = "git.wntrmute.dev/kyle/mcns/gen/mcns/v1"; + +import "google/protobuf/timestamp.proto"; + +service ZoneService { + rpc ListZones(ListZonesRequest) returns (ListZonesResponse); + rpc CreateZone(CreateZoneRequest) returns (Zone); + rpc GetZone(GetZoneRequest) returns (Zone); + rpc UpdateZone(UpdateZoneRequest) returns (Zone); + rpc DeleteZone(DeleteZoneRequest) returns (DeleteZoneResponse); +} + +message Zone { + int64 id = 1; + string name = 2; + string primary_ns = 3; + string admin_email = 4; + int32 refresh = 5; + int32 retry = 6; + int32 expire = 7; + int32 minimum_ttl = 8; + int64 serial = 9; + google.protobuf.Timestamp created_at = 10; + google.protobuf.Timestamp updated_at = 11; +} + +message ListZonesRequest {} + +message ListZonesResponse { + repeated Zone zones = 1; +} + +message CreateZoneRequest { + string name = 1; + string primary_ns = 2; + string admin_email = 3; + int32 refresh = 4; + int32 retry = 5; + int32 expire = 6; + int32 minimum_ttl = 7; +} + +message GetZoneRequest { + string name = 1; +} + +message UpdateZoneRequest { + string name = 1; + string primary_ns = 2; + string admin_email = 3; + int32 refresh = 4; + int32 retry = 5; + int32 expire = 6; + int32 minimum_ttl = 7; +} + +message DeleteZoneRequest { + string name = 1; +} + +message DeleteZoneResponse {} diff --git a/zones/mcp.metacircular.net.zone b/zones/mcp.metacircular.net.zone deleted file mode 100644 index 216ee94..0000000 --- a/zones/mcp.metacircular.net.zone +++ /dev/null @@ -1,26 +0,0 @@ -; Node addresses for Metacircular platform. -; Maps node names to their network addresses. -; -; When MCNS is built, these will be managed via the MCNS API. -; Until then, this file is manually maintained. - -$ORIGIN mcp.metacircular.net. -$TTL 300 - -@ IN SOA ns.mcp.metacircular.net. admin.metacircular.net. ( - 2026032501 ; serial (YYYYMMDDNN) - 3600 ; refresh - 600 ; retry - 86400 ; expire - 300 ; minimum TTL - ) - - IN NS ns.mcp.metacircular.net. - -; --- Nodes --- -rift IN A 192.168.88.181 -rift IN A 100.95.252.120 - -; ns record target — points to rift where CoreDNS runs. -ns IN A 192.168.88.181 -ns IN A 100.95.252.120 diff --git a/zones/svc.mcp.metacircular.net.zone b/zones/svc.mcp.metacircular.net.zone deleted file mode 100644 index 508fd08..0000000 --- a/zones/svc.mcp.metacircular.net.zone +++ /dev/null @@ -1,28 +0,0 @@ -; Internal service addresses for Metacircular platform. -; Maps service names to the node where they currently run. -; -; When MCNS is built, MCP will manage these records dynamically. -; Until then, this file is manually maintained. - -$ORIGIN svc.mcp.metacircular.net. -$TTL 300 - -@ IN SOA ns.mcp.metacircular.net. admin.metacircular.net. ( - 2026032601 ; serial (YYYYMMDDNN) - 3600 ; refresh - 600 ; retry - 86400 ; expire - 300 ; minimum TTL - ) - - IN NS ns.mcp.metacircular.net. - -; --- Services on rift --- -metacrypt IN A 192.168.88.181 -metacrypt IN A 100.95.252.120 -mcr IN A 192.168.88.181 -mcr IN A 100.95.252.120 -sgard IN A 192.168.88.181 -sgard IN A 100.95.252.120 -mcp-agent IN A 192.168.88.181 -mcp-agent IN A 100.95.252.120