Compare commits
5 Commits
v0.1.0
...
unit8-grpc
| Author | SHA1 | Date | |
|---|---|---|---|
| 82b7d295ef | |||
| 4ec0c3a916 | |||
| 5efd51b3d7 | |||
| f9635578e0 | |||
| a545fec658 |
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/mcns
|
||||||
|
srv/
|
||||||
|
*.db
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.DS_Store
|
||||||
90
.golangci.yaml
Normal file
90
.golangci.yaml
Normal file
@@ -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"
|
||||||
423
ARCHITECTURE.md
Normal file
423
ARCHITECTURE.md
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
# MCNS Architecture
|
||||||
|
|
||||||
|
Metacircular Networking Service — an authoritative DNS server for the
|
||||||
|
Metacircular platform with a management API for dynamic record updates.
|
||||||
|
|
||||||
|
## System Overview
|
||||||
|
|
||||||
|
MCNS replaces the CoreDNS precursor with a purpose-built Go DNS server.
|
||||||
|
It serves authoritative DNS for platform zones (e.g.,
|
||||||
|
`svc.mcp.metacircular.net`, `mcp.metacircular.net`) and forwards all
|
||||||
|
other queries to upstream resolvers. Records are stored in SQLite and
|
||||||
|
managed via gRPC and REST APIs authenticated through MCIAS.
|
||||||
|
|
||||||
|
### Architecture Layers
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Clients │
|
||||||
|
│ DNS resolvers (port 53) │ API clients (8443/9443) │
|
||||||
|
└──────────┬────────────────┴──────────────┬──────────────┘
|
||||||
|
│ │
|
||||||
|
┌──────────▼──────────┐ ┌───────────────▼───────────────┐
|
||||||
|
│ DNS Server │ │ Management API │
|
||||||
|
│ UDP+TCP :53 │ │ REST :8443 │ gRPC :9443 │
|
||||||
|
│ miekg/dns │ │ chi router │ grpc-go │
|
||||||
|
└──────────┬──────────┘ └───────────────┬───────────────┘
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────┐ │
|
||||||
|
│ │ Auth Layer │ │
|
||||||
|
│ │ MCIAS via │ │
|
||||||
|
│ │ mcdsl/auth │ │
|
||||||
|
│ └────────┬────────┘ │
|
||||||
|
│ │ │
|
||||||
|
┌──────────▼──────────────────▼────────────▼──────────────┐
|
||||||
|
│ Storage Layer │
|
||||||
|
│ SQLite (zones + records) │
|
||||||
|
└──────────────────────────┬──────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────▼────────────┐
|
||||||
|
│ Upstream Forwarder │
|
||||||
|
│ 1.1.1.1 / 8.8.8.8 │
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
|
||||||
|
1. **DNS is the fast path.** The DNS handler reads from SQLite and
|
||||||
|
responds. No locks beyond SQLite's own WAL reader concurrency. DNS
|
||||||
|
queries never touch the auth layer.
|
||||||
|
2. **Management is the authenticated path.** All record mutations go
|
||||||
|
through the REST/gRPC API, authenticated via MCIAS.
|
||||||
|
3. **Immediate consistency.** Record changes are visible to DNS queries
|
||||||
|
as soon as the SQLite transaction commits. No restart required.
|
||||||
|
4. **Forward everything else.** Non-authoritative queries are forwarded
|
||||||
|
to configured upstream resolvers. Forwarded responses are cached
|
||||||
|
in-memory with TTL-based expiry.
|
||||||
|
|
||||||
|
## Record Types
|
||||||
|
|
||||||
|
v1 supports three record types:
|
||||||
|
|
||||||
|
| Type | Value Format | Example |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| A | IPv4 address | `192.168.88.181` |
|
||||||
|
| AAAA | IPv6 address | `2001:db8::1` |
|
||||||
|
| CNAME | Fully-qualified domain name | `rift.mcp.metacircular.net.` |
|
||||||
|
|
||||||
|
CNAME records follow standard DNS rules: a name with a CNAME record
|
||||||
|
must not have any other record types (enforced at the database level).
|
||||||
|
|
||||||
|
## DNS Server
|
||||||
|
|
||||||
|
### Listening
|
||||||
|
|
||||||
|
The DNS server listens on a configurable address (default `:53`) for
|
||||||
|
both UDP and TCP. UDP is the primary transport; TCP is required for
|
||||||
|
responses exceeding 512 bytes and for zone transfer compatibility.
|
||||||
|
|
||||||
|
### Query Handling
|
||||||
|
|
||||||
|
```
|
||||||
|
Query received
|
||||||
|
│
|
||||||
|
├─ Is the qname within an authoritative zone?
|
||||||
|
│ │
|
||||||
|
│ ├─ Yes → Look up records from SQLite
|
||||||
|
│ │ ├─ Records found → Build authoritative response (AA=1)
|
||||||
|
│ │ └─ No records → Return NXDOMAIN (AA=1, SOA in authority)
|
||||||
|
│ │
|
||||||
|
│ └─ No → Forward to upstream
|
||||||
|
│ ├─ Check cache → Hit → Return cached response
|
||||||
|
│ └─ Miss → Query upstream, cache response, return
|
||||||
|
│
|
||||||
|
└─ Special cases:
|
||||||
|
├─ SOA query for zone apex → Return SOA from zone config
|
||||||
|
└─ NS query for zone apex → Return NS from zone config
|
||||||
|
```
|
||||||
|
|
||||||
|
### SOA Serial Management
|
||||||
|
|
||||||
|
Zone SOA serial numbers use the YYYYMMDDNN format. When a record is
|
||||||
|
created, updated, or deleted, the zone's serial is auto-incremented:
|
||||||
|
|
||||||
|
- If the current serial's date prefix matches today, increment NN.
|
||||||
|
- If the date prefix is older, reset to today with NN=01.
|
||||||
|
- Maximum 99 changes per day per zone before the counter overflows to
|
||||||
|
the next day.
|
||||||
|
|
||||||
|
### Forwarding and Caching
|
||||||
|
|
||||||
|
Non-authoritative queries are forwarded to upstream resolvers configured
|
||||||
|
in `[dns]`. The forwarder:
|
||||||
|
|
||||||
|
- Tries each upstream in order, with a 2-second timeout per attempt.
|
||||||
|
- Caches successful responses keyed by (qname, qtype, qclass).
|
||||||
|
- Cache TTL is the minimum TTL from the response, capped at 300 seconds.
|
||||||
|
- Cache is in-memory with lazy expiry (checked on read).
|
||||||
|
- SERVFAIL and refused responses are not cached.
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
SQLite with WAL mode, opened via `mcdsl/db.Open`. Single database file
|
||||||
|
at the configured path (default `/srv/mcns/mcns.db`).
|
||||||
|
|
||||||
|
### Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Migration 1: zones and records
|
||||||
|
|
||||||
|
CREATE TABLE 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 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 idx_records_zone_name ON records(zone_id, name);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Constraints
|
||||||
|
|
||||||
|
- Zone names are stored without trailing dot, lowercased.
|
||||||
|
- Record names are relative to the zone (e.g., `metacrypt` not
|
||||||
|
`metacrypt.svc.mcp.metacircular.net`). The special name `@` refers
|
||||||
|
to the zone apex.
|
||||||
|
- CNAME exclusivity: inserting a CNAME for a name that already has A/AAAA
|
||||||
|
records (or vice versa) is rejected. Enforced in application logic
|
||||||
|
within the transaction.
|
||||||
|
- A record's value must be a valid IPv4 address. AAAA must be valid IPv6.
|
||||||
|
CNAME must be a valid domain name ending with a dot.
|
||||||
|
|
||||||
|
## Authentication and Authorization
|
||||||
|
|
||||||
|
Authentication is delegated to MCIAS via `mcdsl/auth`. The DNS server
|
||||||
|
(port 53) has no authentication — it serves DNS to any client, as is
|
||||||
|
standard for DNS.
|
||||||
|
|
||||||
|
The management API (REST + gRPC) uses MCIAS bearer tokens:
|
||||||
|
|
||||||
|
| Role | Permissions |
|
||||||
|
|------|-------------|
|
||||||
|
| admin | Full access: create/update/delete zones and records |
|
||||||
|
| user | Read-only: list and get zones and records |
|
||||||
|
| guest | Read-only: list and get zones and records |
|
||||||
|
|
||||||
|
### gRPC Interceptor Maps
|
||||||
|
|
||||||
|
| Map | Methods |
|
||||||
|
|-----|---------|
|
||||||
|
| Public | `Health` |
|
||||||
|
| AuthRequired | `ListZones`, `GetZone`, `ListRecords`, `GetRecord` |
|
||||||
|
| AdminRequired | `CreateZone`, `UpdateZone`, `DeleteZone`, `CreateRecord`, `UpdateRecord`, `DeleteRecord` |
|
||||||
|
|
||||||
|
## API Surface
|
||||||
|
|
||||||
|
### REST Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Auth | Description |
|
||||||
|
|--------|------|------|-------------|
|
||||||
|
| POST | `/v1/auth/login` | None | Login via MCIAS |
|
||||||
|
| POST | `/v1/auth/logout` | Bearer | Logout |
|
||||||
|
| GET | `/v1/health` | None | Health check |
|
||||||
|
| GET | `/v1/zones` | Bearer | List all zones |
|
||||||
|
| POST | `/v1/zones` | Admin | Create a zone |
|
||||||
|
| GET | `/v1/zones/{zone}` | Bearer | Get zone details |
|
||||||
|
| PUT | `/v1/zones/{zone}` | Admin | Update zone SOA parameters |
|
||||||
|
| DELETE | `/v1/zones/{zone}` | Admin | Delete zone and all its records |
|
||||||
|
| GET | `/v1/zones/{zone}/records` | Bearer | List records in a zone |
|
||||||
|
| POST | `/v1/zones/{zone}/records` | Admin | Create a record |
|
||||||
|
| GET | `/v1/zones/{zone}/records/{id}` | Bearer | Get a record |
|
||||||
|
| PUT | `/v1/zones/{zone}/records/{id}` | Admin | Update a record |
|
||||||
|
| DELETE | `/v1/zones/{zone}/records/{id}` | Admin | Delete a record |
|
||||||
|
|
||||||
|
### gRPC Services
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
service AuthService {
|
||||||
|
rpc Login(LoginRequest) returns (LoginResponse);
|
||||||
|
rpc Logout(LogoutRequest) returns (LogoutResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
service AdminService {
|
||||||
|
rpc Health(HealthRequest) returns (HealthResponse);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request/Response Examples
|
||||||
|
|
||||||
|
**Create Zone:**
|
||||||
|
```json
|
||||||
|
POST /v1/zones
|
||||||
|
{
|
||||||
|
"name": "svc.mcp.metacircular.net",
|
||||||
|
"primary_ns": "ns.mcp.metacircular.net.",
|
||||||
|
"admin_email": "admin.metacircular.net."
|
||||||
|
}
|
||||||
|
|
||||||
|
Response 201:
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "svc.mcp.metacircular.net",
|
||||||
|
"primary_ns": "ns.mcp.metacircular.net.",
|
||||||
|
"admin_email": "admin.metacircular.net.",
|
||||||
|
"refresh": 3600,
|
||||||
|
"retry": 600,
|
||||||
|
"expire": 86400,
|
||||||
|
"minimum_ttl": 300,
|
||||||
|
"serial": 2026032601,
|
||||||
|
"created_at": "2026-03-26T20:00:00Z",
|
||||||
|
"updated_at": "2026-03-26T20:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Record:**
|
||||||
|
```json
|
||||||
|
POST /v1/zones/svc.mcp.metacircular.net/records
|
||||||
|
{
|
||||||
|
"name": "metacrypt",
|
||||||
|
"type": "A",
|
||||||
|
"value": "192.168.88.181",
|
||||||
|
"ttl": 300
|
||||||
|
}
|
||||||
|
|
||||||
|
Response 201:
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"zone": "svc.mcp.metacircular.net",
|
||||||
|
"name": "metacrypt",
|
||||||
|
"type": "A",
|
||||||
|
"value": "192.168.88.181",
|
||||||
|
"ttl": 300,
|
||||||
|
"created_at": "2026-03-26T20:00:00Z",
|
||||||
|
"updated_at": "2026-03-26T20:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "CNAME record conflicts with existing A record for 'metacrypt'"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[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"
|
||||||
|
```
|
||||||
|
|
||||||
|
### DNS-Specific Configuration
|
||||||
|
|
||||||
|
| Field | Required | Default | Description |
|
||||||
|
|-------|----------|---------|-------------|
|
||||||
|
| `dns.listen_addr` | No | `:53` | UDP+TCP listen address |
|
||||||
|
| `dns.upstreams` | No | `["1.1.1.1:53", "8.8.8.8:53"]` | Upstream forwarders |
|
||||||
|
|
||||||
|
## CLI Commands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `mcns server` | Start the DNS + API servers |
|
||||||
|
| `mcns snapshot` | Create a database backup |
|
||||||
|
| `mcns status` | Query a running instance's health |
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Container
|
||||||
|
|
||||||
|
Single Dockerfile, single binary. The container exposes:
|
||||||
|
|
||||||
|
- Port 53 (UDP+TCP) — DNS
|
||||||
|
- Port 8443 (TCP) — REST API
|
||||||
|
- Port 9443 (TCP) — gRPC API
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM golang:1.25-alpine AS builder
|
||||||
|
# ... build with CGO_ENABLED=0 ...
|
||||||
|
|
||||||
|
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", "server"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose (rift)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mcns:
|
||||||
|
image: mcr.svc.mcp.metacircular.net:8443/mcns:latest
|
||||||
|
container_name: mcns
|
||||||
|
restart: unless-stopped
|
||||||
|
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:
|
||||||
|
- /srv/mcns:/srv/mcns
|
||||||
|
```
|
||||||
|
|
||||||
|
The API ports (8443/9443) are fronted by MC-Proxy, not exposed directly.
|
||||||
|
|
||||||
|
### Data Directory
|
||||||
|
|
||||||
|
```
|
||||||
|
/srv/mcns/
|
||||||
|
├── mcns.toml Configuration
|
||||||
|
├── mcns.db SQLite database
|
||||||
|
├── certs/ TLS certificates
|
||||||
|
└── backups/ Database snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
|
||||||
|
### Threat Mitigations
|
||||||
|
|
||||||
|
| Threat | Mitigation |
|
||||||
|
|--------|------------|
|
||||||
|
| DNS cache poisoning | Forwarded responses validated by upstream; TXID randomized by miekg/dns |
|
||||||
|
| DNS amplification | Response rate limiting (future); TCP fallback for large responses |
|
||||||
|
| Unauthorized record modification | Admin-only writes via MCIAS-authenticated API |
|
||||||
|
| SQL injection | Parameterized queries throughout |
|
||||||
|
| Zone enumeration via API | Requires authentication (user or admin role) |
|
||||||
|
|
||||||
|
### Security Invariants
|
||||||
|
|
||||||
|
1. DNS port 53 serves read-only data. No mutations possible via DNS protocol.
|
||||||
|
2. All record mutations require admin authentication through MCIAS.
|
||||||
|
3. CNAME exclusivity is enforced transactionally — no name can have both
|
||||||
|
CNAME and address records.
|
||||||
|
4. Zone serial numbers are monotonically increasing — rollback is not possible.
|
||||||
|
5. TLS 1.3 minimum on management API (enforced by mcdsl/httpserver).
|
||||||
|
|
||||||
|
## Future Work
|
||||||
|
|
||||||
|
- **MX, TXT, SRV records** — additional record types as needed.
|
||||||
|
- **DNSSEC** — zone signing for authoritative responses.
|
||||||
|
- **Zone transfer (AXFR/IXFR)** — for secondary DNS servers.
|
||||||
|
- **MCP integration** — MCP agent automatically registers service records.
|
||||||
|
- **Web UI** — htmx interface for zone and record management.
|
||||||
|
- **Rate limiting** — DNS query rate limiting to mitigate amplification.
|
||||||
|
- **Metrics** — Prometheus endpoint for query rates, cache hit ratio, latency.
|
||||||
94
CLAUDE.md
94
CLAUDE.md
@@ -2,46 +2,70 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
MCNS precursor — a CoreDNS instance serving internal DNS zones for the
|
MCNS (Metacircular Networking Service) is an authoritative DNS server for
|
||||||
Metacircular platform until the full MCNS service is built.
|
the Metacircular platform. It serves DNS zones from SQLite, forwards
|
||||||
|
non-authoritative queries to upstream resolvers, and exposes a gRPC/REST
|
||||||
|
management API authenticated through MCIAS.
|
||||||
|
|
||||||
## Zones
|
## Build Commands
|
||||||
|
|
||||||
| Zone | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `svc.mcp.metacircular.net` | Internal service addresses (e.g. `metacrypt.svc.mcp.metacircular.net`) |
|
|
||||||
| `mcp.metacircular.net` | Node addresses (e.g. `rift.mcp.metacircular.net`) |
|
|
||||||
|
|
||||||
Everything else forwards to 1.1.1.1 and 8.8.8.8.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- `Corefile` — CoreDNS configuration
|
|
||||||
- `zones/` — Zone files (manually maintained until MCP manages them)
|
|
||||||
- `deploy/docker/docker-compose-rift.yml` — Docker compose for rift deployment
|
|
||||||
|
|
||||||
## Operations
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start
|
make all # vet → lint → test → build
|
||||||
docker compose -f deploy/docker/docker-compose-rift.yml up -d
|
make build # go build ./...
|
||||||
|
make test # go test ./...
|
||||||
# Test resolution
|
make vet # go vet ./...
|
||||||
dig @192.168.88.181 metacrypt.svc.mcp.metacircular.net
|
make lint # golangci-lint run ./...
|
||||||
dig @192.168.88.181 rift.mcp.metacircular.net
|
make proto # regenerate gRPC code from .proto files
|
||||||
|
make proto-lint # buf lint + buf breaking
|
||||||
# After editing zone files, bump the serial and restart
|
make mcns # build mcns binary with version injection
|
||||||
docker compose -f deploy/docker/docker-compose-rift.yml restart
|
make docker # build container image
|
||||||
|
make clean # remove binaries
|
||||||
|
make devserver # build and run against srv/ config
|
||||||
```
|
```
|
||||||
|
|
||||||
## Adding a service
|
Run a single test: `go test ./internal/dns/ -run TestHandlerA`
|
||||||
|
|
||||||
1. Add an A record to `zones/svc.mcp.metacircular.net.zone`
|
## Architecture
|
||||||
2. Bump the serial number (YYYYMMDDNN format)
|
|
||||||
3. Restart CoreDNS
|
|
||||||
|
|
||||||
## Adding a node
|
Three listeners on one binary:
|
||||||
|
|
||||||
1. Add an A record to `zones/mcp.metacircular.net.zone`
|
- **DNS server** (`:53`, UDP+TCP) — serves authoritative zones from SQLite,
|
||||||
2. Bump the serial number
|
forwards everything else to upstream resolvers. No authentication.
|
||||||
3. Restart CoreDNS
|
- **REST API** (`:8443`) — management API for zones and records. MCIAS auth.
|
||||||
|
- **gRPC API** (`:9443`) — same management operations. MCIAS auth.
|
||||||
|
|
||||||
|
Records stored in SQLite. Changes visible to DNS immediately on commit.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/mcns/ CLI entry point (server, snapshot, status)
|
||||||
|
internal/
|
||||||
|
config/ TOML config with DNS-specific sections
|
||||||
|
db/ SQLite schema, migrations, zone/record queries
|
||||||
|
dns/ DNS server, handler, forwarder, cache
|
||||||
|
server/ REST API routes and handlers
|
||||||
|
grpcserver/ gRPC server, interceptors, service handlers
|
||||||
|
proto/mcns/v1/ Protobuf definitions
|
||||||
|
gen/mcns/v1/ Generated Go code (do not edit)
|
||||||
|
deploy/ Docker, systemd, install scripts, examples
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ignored Directories
|
||||||
|
|
||||||
|
- `srv/` — local dev runtime data
|
||||||
|
- `gen/` — regenerated from proto, not hand-edited
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
1. **REST/gRPC sync**: Every REST endpoint must have a corresponding gRPC
|
||||||
|
RPC, updated in the same change.
|
||||||
|
2. **gRPC interceptor maps**: New RPCs must be added to the correct
|
||||||
|
interceptor map (Public, AuthRequired, AdminRequired). Forgetting this
|
||||||
|
is a security defect.
|
||||||
|
3. **CNAME exclusivity**: A name cannot have both CNAME and A/AAAA records.
|
||||||
|
Enforced in the database layer within a transaction.
|
||||||
|
4. **SOA serial**: Auto-incremented on every record mutation. Never manually
|
||||||
|
set or decremented.
|
||||||
|
5. **DNS has no auth**: Port 53 serves records to any client. All mutations
|
||||||
|
go through the authenticated management API.
|
||||||
|
|||||||
20
Corefile
20
Corefile
@@ -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
|
|
||||||
}
|
|
||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -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"]
|
||||||
40
Makefile
Normal file
40
Makefile
Normal file
@@ -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
|
||||||
9
buf.yaml
Normal file
9
buf.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version: v2
|
||||||
|
modules:
|
||||||
|
- path: proto
|
||||||
|
lint:
|
||||||
|
use:
|
||||||
|
- STANDARD
|
||||||
|
breaking:
|
||||||
|
use:
|
||||||
|
- FILE
|
||||||
278
cmd/mcns/main.go
Normal file
278
cmd/mcns/main.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,18 @@
|
|||||||
# CoreDNS on rift — MCNS precursor.
|
# MCNS on rift — authoritative DNS + management API.
|
||||||
#
|
|
||||||
# Serves the svc.mcp.metacircular.net and mcp.metacircular.net zones.
|
|
||||||
# Forwards everything else to 1.1.1.1 and 8.8.8.8.
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# docker compose -f deploy/docker/docker-compose-rift.yml up -d
|
# 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:
|
services:
|
||||||
coredns:
|
mcns:
|
||||||
image: coredns/coredns:1.12.1
|
image: mcr.svc.mcp.metacircular.net:8443/mcns:latest
|
||||||
container_name: mcns-coredns
|
container_name: mcns
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: -conf /etc/coredns/Corefile
|
command: ["server", "--config", "/srv/mcns/mcns.toml"]
|
||||||
ports:
|
ports:
|
||||||
- "192.168.88.181:53:53/udp"
|
- "192.168.88.181:53:53/udp"
|
||||||
- "192.168.88.181:53:53/tcp"
|
- "192.168.88.181:53:53/tcp"
|
||||||
- "100.95.252.120:53:53/udp"
|
- "100.95.252.120:53:53/udp"
|
||||||
- "100.95.252.120:53:53/tcp"
|
- "100.95.252.120:53:53/tcp"
|
||||||
volumes:
|
volumes:
|
||||||
- ../../Corefile:/etc/coredns/Corefile:ro
|
- /srv/mcns:/srv/mcns
|
||||||
- ../../zones:/etc/coredns/zones:ro
|
|
||||||
|
|||||||
21
deploy/examples/mcns.toml
Normal file
21
deploy/examples/mcns.toml
Normal file
@@ -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"
|
||||||
164
gen/mcns/v1/admin.pb.go
Normal file
164
gen/mcns/v1/admin.pb.go
Normal file
@@ -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 mcnsv1
|
||||||
|
|
||||||
|
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/v1;mcnsv1b\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
|
||||||
|
}
|
||||||
125
gen/mcns/v1/admin_grpc.pb.go
Normal file
125
gen/mcns/v1/admin_grpc.pb.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// 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 mcnsv1
|
||||||
|
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
// AdminService exposes server health and administrative operations.
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
// AdminService exposes server health and administrative operations.
|
||||||
|
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",
|
||||||
|
}
|
||||||
280
gen/mcns/v1/auth.pb.go
Normal file
280
gen/mcns/v1/auth.pb.go
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
// 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 mcnsv1
|
||||||
|
|
||||||
|
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"`
|
||||||
|
// TOTP code for two-factor authentication, if enabled on the account.
|
||||||
|
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/v1;mcnsv1b\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
|
||||||
|
}
|
||||||
163
gen/mcns/v1/auth_grpc.pb.go
Normal file
163
gen/mcns/v1/auth_grpc.pb.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
// 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 mcnsv1
|
||||||
|
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
// AuthService handles authentication by delegating to MCIAS.
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
// AuthService handles authentication by delegating to MCIAS.
|
||||||
|
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",
|
||||||
|
}
|
||||||
623
gen/mcns/v1/record.pb.go
Normal file
623
gen/mcns/v1/record.pb.go
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
// 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 mcnsv1
|
||||||
|
|
||||||
|
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 name this record belongs to (e.g. "example.com.").
|
||||||
|
Zone string `protobuf:"bytes,2,opt,name=zone,proto3" json:"zone,omitempty"`
|
||||||
|
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
// DNS record type (A, AAAA, CNAME, MX, TXT, etc.).
|
||||||
|
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"`
|
||||||
|
// Optional filter by record name.
|
||||||
|
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
// Optional filter by record type (A, AAAA, CNAME, etc.).
|
||||||
|
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 name the record will be created in; must reference an existing zone.
|
||||||
|
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/v1;mcnsv1b\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
|
||||||
|
}
|
||||||
277
gen/mcns/v1/record_grpc.pb.go
Normal file
277
gen/mcns/v1/record_grpc.pb.go
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
// 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 mcnsv1
|
||||||
|
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
// RecordService manages DNS records within zones.
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
// RecordService manages DNS records within zones.
|
||||||
|
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",
|
||||||
|
}
|
||||||
667
gen/mcns/v1/zone.pb.go
Normal file
667
gen/mcns/v1/zone.pb.go
Normal file
@@ -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 mcnsv1
|
||||||
|
|
||||||
|
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/v1;mcnsv1b\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
|
||||||
|
}
|
||||||
277
gen/mcns/v1/zone_grpc.pb.go
Normal file
277
gen/mcns/v1/zone_grpc.pb.go
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
// 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 mcnsv1
|
||||||
|
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
// ZoneService manages DNS zones and their SOA parameters.
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
// ZoneService manages DNS zones and their SOA parameters.
|
||||||
|
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",
|
||||||
|
}
|
||||||
34
go.mod
Normal file
34
go.mod
Normal file
@@ -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
|
||||||
|
)
|
||||||
103
go.sum
Normal file
103
go.sum
Normal file
@@ -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=
|
||||||
49
internal/config/config.go
Normal file
49
internal/config/config.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
23
internal/db/db.go
Normal file
23
internal/db/db.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
74
internal/db/migrate.go
Normal file
74
internal/db/migrate.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
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);`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: 2,
|
||||||
|
Name: "seed zones and records from CoreDNS zone files",
|
||||||
|
SQL: `
|
||||||
|
-- Zone: svc.mcp.metacircular.net (service addresses)
|
||||||
|
INSERT INTO zones (id, name, primary_ns, admin_email, refresh, retry, expire, minimum_ttl, serial)
|
||||||
|
VALUES (1, 'svc.mcp.metacircular.net', 'ns.mcp.metacircular.net.', 'admin.metacircular.net.', 3600, 600, 86400, 300, 2026032601);
|
||||||
|
|
||||||
|
-- Zone: mcp.metacircular.net (node addresses)
|
||||||
|
INSERT INTO zones (id, name, primary_ns, admin_email, refresh, retry, expire, minimum_ttl, serial)
|
||||||
|
VALUES (2, 'mcp.metacircular.net', 'ns.mcp.metacircular.net.', 'admin.metacircular.net.', 3600, 600, 86400, 300, 2026032501);
|
||||||
|
|
||||||
|
-- svc.mcp.metacircular.net records
|
||||||
|
INSERT INTO records (zone_id, name, type, value, ttl) VALUES (1, 'metacrypt', 'A', '192.168.88.181', 300);
|
||||||
|
INSERT INTO records (zone_id, name, type, value, ttl) VALUES (1, 'metacrypt', 'A', '100.95.252.120', 300);
|
||||||
|
INSERT INTO records (zone_id, name, type, value, ttl) VALUES (1, 'mcr', 'A', '192.168.88.181', 300);
|
||||||
|
INSERT INTO records (zone_id, name, type, value, ttl) VALUES (1, 'mcr', 'A', '100.95.252.120', 300);
|
||||||
|
INSERT INTO records (zone_id, name, type, value, ttl) VALUES (1, 'sgard', 'A', '192.168.88.181', 300);
|
||||||
|
INSERT INTO records (zone_id, name, type, value, ttl) VALUES (1, 'sgard', 'A', '100.95.252.120', 300);
|
||||||
|
INSERT INTO records (zone_id, name, type, value, ttl) VALUES (1, 'mcp-agent', 'A', '192.168.88.181', 300);
|
||||||
|
INSERT INTO records (zone_id, name, type, value, ttl) VALUES (1, 'mcp-agent', 'A', '100.95.252.120', 300);
|
||||||
|
|
||||||
|
-- mcp.metacircular.net records
|
||||||
|
INSERT INTO records (zone_id, name, type, value, ttl) VALUES (2, 'rift', 'A', '192.168.88.181', 300);
|
||||||
|
INSERT INTO records (zone_id, name, type, value, ttl) VALUES (2, 'rift', 'A', '100.95.252.120', 300);
|
||||||
|
INSERT INTO records (zone_id, name, type, value, ttl) VALUES (2, 'ns', 'A', '192.168.88.181', 300);
|
||||||
|
INSERT INTO records (zone_id, name, type, value, ttl) VALUES (2, 'ns', 'A', '100.95.252.120', 300);`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate applies all pending migrations.
|
||||||
|
func (d *DB) Migrate() error {
|
||||||
|
return mcdsldb.Migrate(d.DB, Migrations)
|
||||||
|
}
|
||||||
308
internal/db/records.go
Normal file
308
internal/db/records.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
289
internal/db/records_test.go
Normal file
289
internal/db/records_test.go
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createTestZone(t *testing.T, db *DB) *Zone {
|
||||||
|
t.Helper()
|
||||||
|
zone, err := db.CreateZone("test.example.com", "ns.example.com.", "admin.example.com.", 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("test.example.com", "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("test.example.com", "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("test.example.com", "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("test.example.com", "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("test.example.com", "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("test.example.com", "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("test.example.com", "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("test.example.com", "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("test.example.com", "metacrypt", "A", "192.168.88.181", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := db.GetZone("test.example.com")
|
||||||
|
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("test.example.com", "metacrypt", "A", "192.168.88.181", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record 1: %v", err)
|
||||||
|
}
|
||||||
|
_, err = db.CreateRecord("test.example.com", "metacrypt", "A", "100.95.252.120", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record 2: %v", err)
|
||||||
|
}
|
||||||
|
_, err = db.CreateRecord("test.example.com", "mcr", "A", "192.168.88.181", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record 3: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all records.
|
||||||
|
records, err := db.ListRecords("test.example.com", "", "")
|
||||||
|
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("test.example.com", "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("test.example.com", "", "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("test.example.com", "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("test.example.com", "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("test.example.com", "metacrypt", "A", "192.168.88.181", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zone, err := db.GetZone("test.example.com")
|
||||||
|
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("test.example.com")
|
||||||
|
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("test.example.com", "metacrypt", "A", "192.168.88.181", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := db.LookupRecords("test.example.com", "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("test.example.com", "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("test.example.com", "metacrypt", "A", "192.168.88.181", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create first A record: %v", err)
|
||||||
|
}
|
||||||
|
_, err = db.CreateRecord("test.example.com", "metacrypt", "A", "100.95.252.120", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create second A record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := db.LookupRecords("test.example.com", "metacrypt", "A")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("lookup records: %v", err)
|
||||||
|
}
|
||||||
|
if len(records) != 2 {
|
||||||
|
t.Fatalf("got %d records, want 2", len(records))
|
||||||
|
}
|
||||||
|
}
|
||||||
182
internal/db/zones.go
Normal file
182
internal/db/zones.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
168
internal/db/zones_test.go
Normal file
168
internal/db/zones_test.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
// 2 seed zones + 2 created = 4 total. Verify ours are present and ordered.
|
||||||
|
if len(zones) != 4 {
|
||||||
|
t.Fatalf("got %d zones, want 4", 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
67
internal/dns/cache.go
Normal file
67
internal/dns/cache.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
internal/dns/cache_test.go
Normal file
81
internal/dns/cache_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
87
internal/dns/forwarder.go
Normal file
87
internal/dns/forwarder.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
280
internal/dns/server.go
Normal file
280
internal/dns/server.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
142
internal/dns/server_test.go
Normal file
142
internal/dns/server_test.go
Normal file
@@ -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("test.example.com", "ns.example.com.", "admin.example.com.", 3600, 600, 86400, 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create zone: %v", err)
|
||||||
|
}
|
||||||
|
_, err = database.CreateRecord("test.example.com", "metacrypt", "A", "192.168.88.181", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create A record: %v", err)
|
||||||
|
}
|
||||||
|
_, err = database.CreateRecord("test.example.com", "metacrypt", "A", "100.95.252.120", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create A record 2: %v", err)
|
||||||
|
}
|
||||||
|
_, err = database.CreateRecord("test.example.com", "mcr", "AAAA", "2001:db8::1", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create AAAA record: %v", err)
|
||||||
|
}
|
||||||
|
_, err = database.CreateRecord("test.example.com", "alias", "CNAME", "metacrypt.test.example.com.", 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.test.example.com.")
|
||||||
|
if zone == nil {
|
||||||
|
t.Fatal("expected to find zone")
|
||||||
|
}
|
||||||
|
if zone.Name != "test.example.com" {
|
||||||
|
t.Fatalf("got zone %q, want %q", zone.Name, "test.example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
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("test.example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get zone: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
soa := srv.buildSOA(zone)
|
||||||
|
if soa.Ns != "ns.example.com." {
|
||||||
|
t.Fatalf("got ns %q, want %q", soa.Ns, "ns.example.com.")
|
||||||
|
}
|
||||||
|
if soa.Hdr.Name != "test.example.com." {
|
||||||
|
t.Fatalf("got name %q, want %q", soa.Hdr.Name, "test.example.com.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.test.example.com.", 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.test.example.com.", 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.test.example.com.", TTL: 300}
|
||||||
|
rr := srv.recordToRR("alias.test.example.com.", 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.test.example.com." {
|
||||||
|
t.Fatalf("got target %q, want %q", cname.Target, "metacrypt.test.example.com.")
|
||||||
|
}
|
||||||
|
}
|
||||||
20
internal/grpcserver/admin.go
Normal file
20
internal/grpcserver/admin.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
38
internal/grpcserver/auth_handler.go
Normal file
38
internal/grpcserver/auth_handler.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
815
internal/grpcserver/handlers_test.go
Normal file
815
internal/grpcserver/handlers_test.go
Normal file
@@ -0,0 +1,815 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/mcns/gen/mcns/v1"
|
||||||
|
"git.wntrmute.dev/kyle/mcns/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockMCIAS starts a fake MCIAS HTTP server for token validation.
|
||||||
|
// Recognized tokens:
|
||||||
|
// - "admin-token" -> valid, username=admin-uuid, roles=[admin]
|
||||||
|
// - "user-token" -> valid, username=user-uuid, roles=[user]
|
||||||
|
// - anything else -> invalid
|
||||||
|
func mockMCIAS(t *testing.T) *httptest.Server {
|
||||||
|
t.Helper()
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("POST /v1/token/validate", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
switch req.Token {
|
||||||
|
case "admin-token":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"valid": true,
|
||||||
|
"username": "admin-uuid",
|
||||||
|
"account_type": "human",
|
||||||
|
"roles": []string{"admin"},
|
||||||
|
})
|
||||||
|
case "user-token":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"valid": true,
|
||||||
|
"username": "user-uuid",
|
||||||
|
"account_type": "human",
|
||||||
|
"roles": []string{"user"},
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"valid": false})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
srv := httptest.NewServer(mux)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
// testAuthenticator creates an mcdsl/auth.Authenticator that talks to the given mock MCIAS.
|
||||||
|
func testAuthenticator(t *testing.T, serverURL string) *mcdslauth.Authenticator {
|
||||||
|
t.Helper()
|
||||||
|
a, err := mcdslauth.New(mcdslauth.Config{ServerURL: serverURL}, slog.Default())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("auth.New: %v", err)
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// openTestDB creates a temporary test database with migrations applied.
|
||||||
|
func openTestDB(t *testing.T) *db.DB {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join(t.TempDir(), "test.db")
|
||||||
|
d, err := db.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = d.Close() })
|
||||||
|
if err := d.Migrate(); err != nil {
|
||||||
|
t.Fatalf("Migrate: %v", err)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// startTestServer creates a gRPC server with auth interceptors and returns
|
||||||
|
// a connected client. Passing empty cert/key strings skips TLS.
|
||||||
|
func startTestServer(t *testing.T, deps Deps) *grpc.ClientConn {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
srv, err := New("", "", deps, slog.Default())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Listen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = srv.Serve(lis)
|
||||||
|
}()
|
||||||
|
t.Cleanup(func() { srv.GracefulStop() })
|
||||||
|
|
||||||
|
//nolint:gosec // insecure credentials for testing only
|
||||||
|
cc, err := grpc.NewClient(
|
||||||
|
lis.Addr().String(),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Dial: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = cc.Close() })
|
||||||
|
|
||||||
|
return cc
|
||||||
|
}
|
||||||
|
|
||||||
|
// withAuth adds a bearer token to the outgoing context metadata.
|
||||||
|
func withAuth(ctx context.Context, token string) context.Context {
|
||||||
|
return metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// seedZone creates a test zone and returns it.
|
||||||
|
func seedZone(t *testing.T, database *db.DB, name string) *db.Zone {
|
||||||
|
t.Helper()
|
||||||
|
zone, err := database.CreateZone(name, "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("seed zone %q: %v", name, err)
|
||||||
|
}
|
||||||
|
return zone
|
||||||
|
}
|
||||||
|
|
||||||
|
// seedRecord creates a test A record and returns it.
|
||||||
|
func seedRecord(t *testing.T, database *db.DB, zoneName, name, value string) *db.Record {
|
||||||
|
t.Helper()
|
||||||
|
rec, err := database.CreateRecord(zoneName, name, "A", value, 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("seed record %s.%s: %v", name, zoneName, err)
|
||||||
|
}
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Admin tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHealthBypassesAuth(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
client := pb.NewAdminServiceClient(cc)
|
||||||
|
|
||||||
|
// No auth token -- should still succeed because Health is public.
|
||||||
|
resp, err := client.Health(context.Background(), &pb.HealthRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Health should not require auth: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Status != "ok" {
|
||||||
|
t.Fatalf("Health status: got %q, want %q", resp.Status, "ok")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Zone tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestListZones(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "user-token")
|
||||||
|
client := pb.NewZoneServiceClient(cc)
|
||||||
|
|
||||||
|
resp, err := client.ListZones(ctx, &pb.ListZonesRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListZones: %v", err)
|
||||||
|
}
|
||||||
|
// Seed migration creates 2 zones.
|
||||||
|
if len(resp.Zones) != 2 {
|
||||||
|
t.Fatalf("got %d zones, want 2 (seed zones)", len(resp.Zones))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetZoneFound(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
seedZone(t, database, "example.com")
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "user-token")
|
||||||
|
client := pb.NewZoneServiceClient(cc)
|
||||||
|
|
||||||
|
zone, err := client.GetZone(ctx, &pb.GetZoneRequest{Name: "example.com"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetZone: %v", err)
|
||||||
|
}
|
||||||
|
if zone.Name != "example.com" {
|
||||||
|
t.Fatalf("got name %q, want %q", zone.Name, "example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetZoneNotFound(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "user-token")
|
||||||
|
client := pb.NewZoneServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.GetZone(ctx, &pb.GetZoneRequest{Name: "nonexistent.com"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nonexistent zone")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.NotFound {
|
||||||
|
t.Fatalf("code: got %v, want NotFound", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateZoneSuccess(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewZoneServiceClient(cc)
|
||||||
|
|
||||||
|
zone, err := client.CreateZone(ctx, &pb.CreateZoneRequest{
|
||||||
|
Name: "newzone.com",
|
||||||
|
PrimaryNs: "ns1.newzone.com.",
|
||||||
|
AdminEmail: "admin.newzone.com.",
|
||||||
|
Refresh: 3600,
|
||||||
|
Retry: 600,
|
||||||
|
Expire: 86400,
|
||||||
|
MinimumTtl: 300,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateZone: %v", err)
|
||||||
|
}
|
||||||
|
if zone.Name != "newzone.com" {
|
||||||
|
t.Fatalf("got name %q, want %q", zone.Name, "newzone.com")
|
||||||
|
}
|
||||||
|
if zone.Serial == 0 {
|
||||||
|
t.Fatal("serial should not be zero")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateZoneDuplicate(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
seedZone(t, database, "example.com")
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewZoneServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.CreateZone(ctx, &pb.CreateZoneRequest{
|
||||||
|
Name: "example.com",
|
||||||
|
PrimaryNs: "ns1.example.com.",
|
||||||
|
AdminEmail: "admin.example.com.",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for duplicate zone")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.AlreadyExists {
|
||||||
|
t.Fatalf("code: got %v, want AlreadyExists", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateZone(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
original := seedZone(t, database, "example.com")
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewZoneServiceClient(cc)
|
||||||
|
|
||||||
|
updated, err := client.UpdateZone(ctx, &pb.UpdateZoneRequest{
|
||||||
|
Name: "example.com",
|
||||||
|
PrimaryNs: "ns2.example.com.",
|
||||||
|
AdminEmail: "newadmin.example.com.",
|
||||||
|
Refresh: 7200,
|
||||||
|
Retry: 1200,
|
||||||
|
Expire: 172800,
|
||||||
|
MinimumTtl: 600,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpdateZone: %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) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
seedZone(t, database, "example.com")
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewZoneServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.DeleteZone(ctx, &pb.DeleteZoneRequest{Name: "example.com"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteZone: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it is gone.
|
||||||
|
_, err = client.GetZone(withAuth(context.Background(), "user-token"), &pb.GetZoneRequest{Name: "example.com"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected NotFound after delete")
|
||||||
|
}
|
||||||
|
st, _ := status.FromError(err)
|
||||||
|
if st.Code() != codes.NotFound {
|
||||||
|
t.Fatalf("code: got %v, want NotFound", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteZoneNotFound(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewZoneServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.DeleteZone(ctx, &pb.DeleteZoneRequest{Name: "nonexistent.com"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nonexistent zone")
|
||||||
|
}
|
||||||
|
st, _ := status.FromError(err)
|
||||||
|
if st.Code() != codes.NotFound {
|
||||||
|
t.Fatalf("code: got %v, want NotFound", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Record tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestListRecords(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
seedZone(t, database, "example.com")
|
||||||
|
seedRecord(t, database, "example.com", "www", "10.0.0.1")
|
||||||
|
seedRecord(t, database, "example.com", "mail", "10.0.0.2")
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "user-token")
|
||||||
|
client := pb.NewRecordServiceClient(cc)
|
||||||
|
|
||||||
|
resp, err := client.ListRecords(ctx, &pb.ListRecordsRequest{Zone: "example.com"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListRecords: %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.Records) != 2 {
|
||||||
|
t.Fatalf("got %d records, want 2", len(resp.Records))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRecordFound(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
seedZone(t, database, "example.com")
|
||||||
|
rec := seedRecord(t, database, "example.com", "www", "10.0.0.1")
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "user-token")
|
||||||
|
client := pb.NewRecordServiceClient(cc)
|
||||||
|
|
||||||
|
got, err := client.GetRecord(ctx, &pb.GetRecordRequest{Id: rec.ID})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRecord: %v", err)
|
||||||
|
}
|
||||||
|
if got.Name != "www" {
|
||||||
|
t.Fatalf("got name %q, want %q", got.Name, "www")
|
||||||
|
}
|
||||||
|
if got.Value != "10.0.0.1" {
|
||||||
|
t.Fatalf("got value %q, want %q", got.Value, "10.0.0.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRecordNotFound(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "user-token")
|
||||||
|
client := pb.NewRecordServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.GetRecord(ctx, &pb.GetRecordRequest{Id: 999999})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nonexistent record")
|
||||||
|
}
|
||||||
|
st, _ := status.FromError(err)
|
||||||
|
if st.Code() != codes.NotFound {
|
||||||
|
t.Fatalf("code: got %v, want NotFound", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRecordSuccess(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
seedZone(t, database, "example.com")
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewRecordServiceClient(cc)
|
||||||
|
|
||||||
|
rec, err := client.CreateRecord(ctx, &pb.CreateRecordRequest{
|
||||||
|
Zone: "example.com",
|
||||||
|
Name: "www",
|
||||||
|
Type: "A",
|
||||||
|
Value: "10.0.0.1",
|
||||||
|
Ttl: 300,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateRecord: %v", err)
|
||||||
|
}
|
||||||
|
if rec.Name != "www" {
|
||||||
|
t.Fatalf("got name %q, want %q", rec.Name, "www")
|
||||||
|
}
|
||||||
|
if rec.Type != "A" {
|
||||||
|
t.Fatalf("got type %q, want %q", rec.Type, "A")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRecordInvalidValue(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
seedZone(t, database, "example.com")
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewRecordServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.CreateRecord(ctx, &pb.CreateRecordRequest{
|
||||||
|
Zone: "example.com",
|
||||||
|
Name: "www",
|
||||||
|
Type: "A",
|
||||||
|
Value: "not-an-ip",
|
||||||
|
Ttl: 300,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for invalid A record value")
|
||||||
|
}
|
||||||
|
st, _ := status.FromError(err)
|
||||||
|
if st.Code() != codes.InvalidArgument {
|
||||||
|
t.Fatalf("code: got %v, want InvalidArgument", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRecordCNAMEConflict(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
seedZone(t, database, "example.com")
|
||||||
|
seedRecord(t, database, "example.com", "www", "10.0.0.1")
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewRecordServiceClient(cc)
|
||||||
|
|
||||||
|
// Try to create a CNAME for "www" which already has an A record.
|
||||||
|
_, err := client.CreateRecord(ctx, &pb.CreateRecordRequest{
|
||||||
|
Zone: "example.com",
|
||||||
|
Name: "www",
|
||||||
|
Type: "CNAME",
|
||||||
|
Value: "other.example.com.",
|
||||||
|
Ttl: 300,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for CNAME conflict with existing A record")
|
||||||
|
}
|
||||||
|
st, _ := status.FromError(err)
|
||||||
|
if st.Code() != codes.AlreadyExists {
|
||||||
|
t.Fatalf("code: got %v, want AlreadyExists", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRecordAConflictWithCNAME(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
seedZone(t, database, "example.com")
|
||||||
|
|
||||||
|
// Create a CNAME first.
|
||||||
|
_, err := database.CreateRecord("example.com", "alias", "CNAME", "target.example.com.", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("seed CNAME: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewRecordServiceClient(cc)
|
||||||
|
|
||||||
|
// Try to create an A record for "alias" which already has a CNAME.
|
||||||
|
_, err = client.CreateRecord(ctx, &pb.CreateRecordRequest{
|
||||||
|
Zone: "example.com",
|
||||||
|
Name: "alias",
|
||||||
|
Type: "A",
|
||||||
|
Value: "10.0.0.1",
|
||||||
|
Ttl: 300,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for A record conflict with existing CNAME")
|
||||||
|
}
|
||||||
|
st, _ := status.FromError(err)
|
||||||
|
if st.Code() != codes.AlreadyExists {
|
||||||
|
t.Fatalf("code: got %v, want AlreadyExists", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateRecord(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
seedZone(t, database, "example.com")
|
||||||
|
rec := seedRecord(t, database, "example.com", "www", "10.0.0.1")
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewRecordServiceClient(cc)
|
||||||
|
|
||||||
|
updated, err := client.UpdateRecord(ctx, &pb.UpdateRecordRequest{
|
||||||
|
Id: rec.ID,
|
||||||
|
Name: "www",
|
||||||
|
Type: "A",
|
||||||
|
Value: "10.0.0.2",
|
||||||
|
Ttl: 600,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpdateRecord: %v", err)
|
||||||
|
}
|
||||||
|
if updated.Value != "10.0.0.2" {
|
||||||
|
t.Fatalf("got value %q, want %q", updated.Value, "10.0.0.2")
|
||||||
|
}
|
||||||
|
if updated.Ttl != 600 {
|
||||||
|
t.Fatalf("got ttl %d, want 600", updated.Ttl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateRecordNotFound(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewRecordServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.UpdateRecord(ctx, &pb.UpdateRecordRequest{
|
||||||
|
Id: 999999,
|
||||||
|
Name: "www",
|
||||||
|
Type: "A",
|
||||||
|
Value: "10.0.0.1",
|
||||||
|
Ttl: 300,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nonexistent record")
|
||||||
|
}
|
||||||
|
st, _ := status.FromError(err)
|
||||||
|
if st.Code() != codes.NotFound {
|
||||||
|
t.Fatalf("code: got %v, want NotFound", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRecord(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
seedZone(t, database, "example.com")
|
||||||
|
rec := seedRecord(t, database, "example.com", "www", "10.0.0.1")
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewRecordServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.DeleteRecord(ctx, &pb.DeleteRecordRequest{Id: rec.ID})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteRecord: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it is gone.
|
||||||
|
_, err = client.GetRecord(withAuth(context.Background(), "user-token"), &pb.GetRecordRequest{Id: rec.ID})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected NotFound after delete")
|
||||||
|
}
|
||||||
|
st, _ := status.FromError(err)
|
||||||
|
if st.Code() != codes.NotFound {
|
||||||
|
t.Fatalf("code: got %v, want NotFound", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRecordNotFound(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewRecordServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.DeleteRecord(ctx, &pb.DeleteRecordRequest{Id: 999999})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nonexistent record")
|
||||||
|
}
|
||||||
|
st, _ := status.FromError(err)
|
||||||
|
if st.Code() != codes.NotFound {
|
||||||
|
t.Fatalf("code: got %v, want NotFound", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Auth interceptor tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestAuthRequiredNoToken(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
client := pb.NewZoneServiceClient(cc)
|
||||||
|
|
||||||
|
// No auth token on an auth-required method.
|
||||||
|
_, err := client.ListZones(context.Background(), &pb.ListZonesRequest{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for unauthenticated request")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status error, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.Unauthenticated {
|
||||||
|
t.Fatalf("code: got %v, want Unauthenticated", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthRequiredInvalidToken(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "bad-token")
|
||||||
|
client := pb.NewZoneServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.ListZones(ctx, &pb.ListZonesRequest{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for invalid token")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status error, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.Unauthenticated {
|
||||||
|
t.Fatalf("code: got %v, want Unauthenticated", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdminRequiredDeniedForUser(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "user-token")
|
||||||
|
client := pb.NewZoneServiceClient(cc)
|
||||||
|
|
||||||
|
// CreateZone requires admin.
|
||||||
|
_, err := client.CreateZone(ctx, &pb.CreateZoneRequest{
|
||||||
|
Name: "forbidden.com",
|
||||||
|
PrimaryNs: "ns1.forbidden.com.",
|
||||||
|
AdminEmail: "admin.forbidden.com.",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-admin user")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status error, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.PermissionDenied {
|
||||||
|
t.Fatalf("code: got %v, want PermissionDenied", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdminRequiredAllowedForAdmin(t *testing.T) {
|
||||||
|
mcias := mockMCIAS(t)
|
||||||
|
auth := testAuthenticator(t, mcias.URL)
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{DB: database, Authenticator: auth})
|
||||||
|
ctx := withAuth(context.Background(), "admin-token")
|
||||||
|
client := pb.NewZoneServiceClient(cc)
|
||||||
|
|
||||||
|
// Admin should be able to create zones.
|
||||||
|
zone, err := client.CreateZone(ctx, &pb.CreateZoneRequest{
|
||||||
|
Name: "admin-created.com",
|
||||||
|
PrimaryNs: "ns1.admin-created.com.",
|
||||||
|
AdminEmail: "admin.admin-created.com.",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateZone as admin: %v", err)
|
||||||
|
}
|
||||||
|
if zone.Name != "admin-created.com" {
|
||||||
|
t.Fatalf("got name %q, want %q", zone.Name, "admin-created.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Interceptor map completeness test
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestMethodMapCompleteness(t *testing.T) {
|
||||||
|
mm := methodMap()
|
||||||
|
|
||||||
|
expectedPublic := []string{
|
||||||
|
"/mcns.v1.AdminService/Health",
|
||||||
|
"/mcns.v1.AuthService/Login",
|
||||||
|
}
|
||||||
|
for _, method := range expectedPublic {
|
||||||
|
if !mm.Public[method] {
|
||||||
|
t.Errorf("method %s should be public but is not in Public", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(mm.Public) != len(expectedPublic) {
|
||||||
|
t.Errorf("Public has %d entries, expected %d", len(mm.Public), len(expectedPublic))
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedAuth := []string{
|
||||||
|
"/mcns.v1.AuthService/Logout",
|
||||||
|
"/mcns.v1.ZoneService/ListZones",
|
||||||
|
"/mcns.v1.ZoneService/GetZone",
|
||||||
|
"/mcns.v1.RecordService/ListRecords",
|
||||||
|
"/mcns.v1.RecordService/GetRecord",
|
||||||
|
}
|
||||||
|
for _, method := range expectedAuth {
|
||||||
|
if !mm.AuthRequired[method] {
|
||||||
|
t.Errorf("method %s should require auth but is not in AuthRequired", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(mm.AuthRequired) != len(expectedAuth) {
|
||||||
|
t.Errorf("AuthRequired has %d entries, expected %d", len(mm.AuthRequired), len(expectedAuth))
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedAdmin := []string{
|
||||||
|
"/mcns.v1.ZoneService/CreateZone",
|
||||||
|
"/mcns.v1.ZoneService/UpdateZone",
|
||||||
|
"/mcns.v1.ZoneService/DeleteZone",
|
||||||
|
"/mcns.v1.RecordService/CreateRecord",
|
||||||
|
"/mcns.v1.RecordService/UpdateRecord",
|
||||||
|
"/mcns.v1.RecordService/DeleteRecord",
|
||||||
|
}
|
||||||
|
for _, method := range expectedAdmin {
|
||||||
|
if !mm.AdminRequired[method] {
|
||||||
|
t.Errorf("method %s should require admin but is not in AdminRequired", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(mm.AdminRequired) != len(expectedAdmin) {
|
||||||
|
t.Errorf("AdminRequired has %d entries, expected %d", len(mm.AdminRequired), len(expectedAdmin))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no method appears in multiple maps (each RPC in exactly one map).
|
||||||
|
all := make(map[string]int)
|
||||||
|
for k := range mm.Public {
|
||||||
|
all[k]++
|
||||||
|
}
|
||||||
|
for k := range mm.AuthRequired {
|
||||||
|
all[k]++
|
||||||
|
}
|
||||||
|
for k := range mm.AdminRequired {
|
||||||
|
all[k]++
|
||||||
|
}
|
||||||
|
for method, count := range all {
|
||||||
|
if count != 1 {
|
||||||
|
t.Errorf("method %s appears in %d maps, expected exactly 1", method, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
internal/grpcserver/interceptors.go
Normal file
45
internal/grpcserver/interceptors.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
110
internal/grpcserver/records.go
Normal file
110
internal/grpcserver/records.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
50
internal/grpcserver/server.go
Normal file
50
internal/grpcserver/server.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
134
internal/grpcserver/zones.go
Normal file
134
internal/grpcserver/zones.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
62
internal/server/auth.go
Normal file
62
internal/server/auth.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
949
internal/server/handlers_test.go
Normal file
949
internal/server/handlers_test.go
Normal file
@@ -0,0 +1,949 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
|
mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth"
|
||||||
|
"git.wntrmute.dev/kyle/mcns/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// openTestDB creates a temporary SQLite database with all migrations applied.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestZone inserts a zone for use by record tests.
|
||||||
|
func createTestZone(t *testing.T, database *db.DB) *db.Zone {
|
||||||
|
t.Helper()
|
||||||
|
zone, err := database.CreateZone("test.example.com", "ns.example.com.", "admin.example.com.", 3600, 600, 86400, 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create zone: %v", err)
|
||||||
|
}
|
||||||
|
return zone
|
||||||
|
}
|
||||||
|
|
||||||
|
// newChiRequest builds a request with chi URL params injected into the context.
|
||||||
|
func newChiRequest(method, target string, body string, params map[string]string) *http.Request {
|
||||||
|
var r *http.Request
|
||||||
|
if body != "" {
|
||||||
|
r = httptest.NewRequest(method, target, strings.NewReader(body))
|
||||||
|
} else {
|
||||||
|
r = httptest.NewRequest(method, target, nil)
|
||||||
|
}
|
||||||
|
r.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if len(params) > 0 {
|
||||||
|
rctx := chi.NewRouteContext()
|
||||||
|
for k, v := range params {
|
||||||
|
rctx.URLParams.Add(k, v)
|
||||||
|
}
|
||||||
|
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeJSON decodes the response body into v.
|
||||||
|
func decodeJSON(t *testing.T, rec *httptest.ResponseRecorder, v any) {
|
||||||
|
t.Helper()
|
||||||
|
if err := json.NewDecoder(rec.Body).Decode(v); err != nil {
|
||||||
|
t.Fatalf("decode json: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Zone handler tests ----
|
||||||
|
|
||||||
|
func TestListZonesHandler_SeedOnly(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
handler := listZonesHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodGet, "/v1/zones", "", nil)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string][]db.Zone
|
||||||
|
decodeJSON(t, rec, &resp)
|
||||||
|
zones := resp["zones"]
|
||||||
|
if len(zones) != 2 {
|
||||||
|
t.Fatalf("got %d zones, want 2 (seed zones)", len(zones))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListZonesHandler_Populated(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
handler := listZonesHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodGet, "/v1/zones", "", nil)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string][]db.Zone
|
||||||
|
decodeJSON(t, rec, &resp)
|
||||||
|
zones := resp["zones"]
|
||||||
|
// 2 seed + 1 created = 3.
|
||||||
|
if len(zones) != 3 {
|
||||||
|
t.Fatalf("got %d zones, want 3", len(zones))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetZoneHandler_Found(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
handler := getZoneHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodGet, "/v1/zones/test.example.com", "", map[string]string{"zone": "test.example.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zone db.Zone
|
||||||
|
decodeJSON(t, rec, &zone)
|
||||||
|
if zone.Name != "test.example.com" {
|
||||||
|
t.Fatalf("zone name = %q, want %q", zone.Name, "test.example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetZoneHandler_NotFound(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
handler := getZoneHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodGet, "/v1/zones/nonexistent.com", "", map[string]string{"zone": "nonexistent.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateZoneHandler_Success(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
body := `{"name":"new.example.com","primary_ns":"ns1.example.com.","admin_email":"admin.example.com."}`
|
||||||
|
handler := createZoneHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPost, "/v1/zones", body, nil)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusCreated {
|
||||||
|
t.Fatalf("status = %d, want %d; body: %s", rec.Code, http.StatusCreated, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var zone db.Zone
|
||||||
|
decodeJSON(t, rec, &zone)
|
||||||
|
if zone.Name != "new.example.com" {
|
||||||
|
t.Fatalf("zone name = %q, want %q", zone.Name, "new.example.com")
|
||||||
|
}
|
||||||
|
if zone.PrimaryNS != "ns1.example.com." {
|
||||||
|
t.Fatalf("primary_ns = %q, want %q", zone.PrimaryNS, "ns1.example.com.")
|
||||||
|
}
|
||||||
|
// SOA defaults should be applied.
|
||||||
|
if zone.Refresh != 3600 {
|
||||||
|
t.Fatalf("refresh = %d, want 3600", zone.Refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateZoneHandler_MissingFields(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
body string
|
||||||
|
}{
|
||||||
|
{"missing name", `{"primary_ns":"ns1.example.com.","admin_email":"admin.example.com."}`},
|
||||||
|
{"missing primary_ns", `{"name":"new.example.com","admin_email":"admin.example.com."}`},
|
||||||
|
{"missing admin_email", `{"name":"new.example.com","primary_ns":"ns1.example.com."}`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
handler := createZoneHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPost, "/v1/zones", tt.body, nil)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateZoneHandler_Duplicate(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
body := `{"name":"test.example.com","primary_ns":"ns1.example.com.","admin_email":"admin.example.com."}`
|
||||||
|
handler := createZoneHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPost, "/v1/zones", body, nil)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusConflict {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusConflict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateZoneHandler_InvalidJSON(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
handler := createZoneHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPost, "/v1/zones", "not json", nil)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateZoneHandler_Success(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
body := `{"primary_ns":"ns2.example.com.","admin_email":"newadmin.example.com.","refresh":7200}`
|
||||||
|
handler := updateZoneHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPut, "/v1/zones/test.example.com", body, map[string]string{"zone": "test.example.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var zone db.Zone
|
||||||
|
decodeJSON(t, rec, &zone)
|
||||||
|
if zone.PrimaryNS != "ns2.example.com." {
|
||||||
|
t.Fatalf("primary_ns = %q, want %q", zone.PrimaryNS, "ns2.example.com.")
|
||||||
|
}
|
||||||
|
if zone.Refresh != 7200 {
|
||||||
|
t.Fatalf("refresh = %d, want 7200", zone.Refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateZoneHandler_NotFound(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
body := `{"primary_ns":"ns2.example.com.","admin_email":"admin.example.com."}`
|
||||||
|
handler := updateZoneHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPut, "/v1/zones/nonexistent.com", body, map[string]string{"zone": "nonexistent.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateZoneHandler_MissingFields(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
body := `{"admin_email":"admin.example.com."}`
|
||||||
|
handler := updateZoneHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPut, "/v1/zones/test.example.com", body, map[string]string{"zone": "test.example.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteZoneHandler_Success(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
handler := deleteZoneHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodDelete, "/v1/zones/test.example.com", "", map[string]string{"zone": "test.example.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNoContent {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the zone is gone.
|
||||||
|
_, err := database.GetZone("test.example.com")
|
||||||
|
if err != db.ErrNotFound {
|
||||||
|
t.Fatalf("expected ErrNotFound after delete, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteZoneHandler_NotFound(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
handler := deleteZoneHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodDelete, "/v1/zones/nonexistent.com", "", map[string]string{"zone": "nonexistent.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Record handler tests ----
|
||||||
|
|
||||||
|
func TestListRecordsHandler_WithZone(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
_, err := database.CreateRecord("test.example.com", "www", "A", "10.0.0.1", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record: %v", err)
|
||||||
|
}
|
||||||
|
_, err = database.CreateRecord("test.example.com", "mail", "A", "10.0.0.2", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := listRecordsHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodGet, "/v1/zones/test.example.com/records", "", map[string]string{"zone": "test.example.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string][]db.Record
|
||||||
|
decodeJSON(t, rec, &resp)
|
||||||
|
records := resp["records"]
|
||||||
|
if len(records) != 2 {
|
||||||
|
t.Fatalf("got %d records, want 2", len(records))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListRecordsHandler_ZoneNotFound(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
handler := listRecordsHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodGet, "/v1/zones/nonexistent.com/records", "", map[string]string{"zone": "nonexistent.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListRecordsHandler_EmptyZone(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
handler := listRecordsHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodGet, "/v1/zones/test.example.com/records", "", map[string]string{"zone": "test.example.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string][]db.Record
|
||||||
|
decodeJSON(t, rec, &resp)
|
||||||
|
records := resp["records"]
|
||||||
|
if len(records) != 0 {
|
||||||
|
t.Fatalf("got %d records, want 0", len(records))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListRecordsHandler_WithFilters(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
_, err := database.CreateRecord("test.example.com", "www", "A", "10.0.0.1", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record: %v", err)
|
||||||
|
}
|
||||||
|
_, err = database.CreateRecord("test.example.com", "www", "A", "10.0.0.2", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record: %v", err)
|
||||||
|
}
|
||||||
|
_, err = database.CreateRecord("test.example.com", "mail", "A", "10.0.0.3", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := listRecordsHandler(database)
|
||||||
|
|
||||||
|
// Filter by name.
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodGet, "/v1/zones/test.example.com/records?name=www", "", map[string]string{"zone": "test.example.com"})
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string][]db.Record
|
||||||
|
decodeJSON(t, rec, &resp)
|
||||||
|
if len(resp["records"]) != 2 {
|
||||||
|
t.Fatalf("got %d records for name=www, want 2", len(resp["records"]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRecordHandler_Found(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
created, err := database.CreateRecord("test.example.com", "www", "A", "10.0.0.1", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := getRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
idStr := fmt.Sprintf("%d", created.ID)
|
||||||
|
req := newChiRequest(http.MethodGet, "/v1/zones/test.example.com/records/"+idStr, "", map[string]string{
|
||||||
|
"zone": "test.example.com",
|
||||||
|
"id": idStr,
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
var record db.Record
|
||||||
|
decodeJSON(t, rec, &record)
|
||||||
|
if record.Name != "www" {
|
||||||
|
t.Fatalf("record name = %q, want %q", record.Name, "www")
|
||||||
|
}
|
||||||
|
if record.Value != "10.0.0.1" {
|
||||||
|
t.Fatalf("record value = %q, want %q", record.Value, "10.0.0.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRecordHandler_NotFound(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
handler := getRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodGet, "/v1/zones/test.example.com/records/99999", "", map[string]string{
|
||||||
|
"zone": "test.example.com",
|
||||||
|
"id": "99999",
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRecordHandler_InvalidID(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
handler := getRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodGet, "/v1/zones/test.example.com/records/abc", "", map[string]string{
|
||||||
|
"zone": "test.example.com",
|
||||||
|
"id": "abc",
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRecordHandler_Success(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
body := `{"name":"www","type":"A","value":"10.0.0.1","ttl":600}`
|
||||||
|
handler := createRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPost, "/v1/zones/test.example.com/records", body, map[string]string{"zone": "test.example.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusCreated {
|
||||||
|
t.Fatalf("status = %d, want %d; body: %s", rec.Code, http.StatusCreated, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var record db.Record
|
||||||
|
decodeJSON(t, rec, &record)
|
||||||
|
if record.Name != "www" {
|
||||||
|
t.Fatalf("record name = %q, want %q", record.Name, "www")
|
||||||
|
}
|
||||||
|
if record.Type != "A" {
|
||||||
|
t.Fatalf("record type = %q, want %q", record.Type, "A")
|
||||||
|
}
|
||||||
|
if record.TTL != 600 {
|
||||||
|
t.Fatalf("ttl = %d, want 600", record.TTL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRecordHandler_MissingFields(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
body string
|
||||||
|
}{
|
||||||
|
{"missing name", `{"type":"A","value":"10.0.0.1"}`},
|
||||||
|
{"missing type", `{"name":"www","value":"10.0.0.1"}`},
|
||||||
|
{"missing value", `{"name":"www","type":"A"}`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
handler := createRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPost, "/v1/zones/test.example.com/records", tt.body, map[string]string{"zone": "test.example.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRecordHandler_InvalidIP(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
body := `{"name":"www","type":"A","value":"not-an-ip"}`
|
||||||
|
handler := createRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPost, "/v1/zones/test.example.com/records", body, map[string]string{"zone": "test.example.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRecordHandler_CNAMEConflict(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
// Create an A record first.
|
||||||
|
_, err := database.CreateRecord("test.example.com", "www", "A", "10.0.0.1", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create A record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to create a CNAME for the same name via handler.
|
||||||
|
body := `{"name":"www","type":"CNAME","value":"other.example.com."}`
|
||||||
|
handler := createRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPost, "/v1/zones/test.example.com/records", body, map[string]string{"zone": "test.example.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusConflict {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusConflict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRecordHandler_ZoneNotFound(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
body := `{"name":"www","type":"A","value":"10.0.0.1"}`
|
||||||
|
handler := createRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPost, "/v1/zones/nonexistent.com/records", body, map[string]string{"zone": "nonexistent.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRecordHandler_InvalidJSON(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
handler := createRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPost, "/v1/zones/test.example.com/records", "not json", map[string]string{"zone": "test.example.com"})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateRecordHandler_Success(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
created, err := database.CreateRecord("test.example.com", "www", "A", "10.0.0.1", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idStr := fmt.Sprintf("%d", created.ID)
|
||||||
|
body := `{"name":"www","type":"A","value":"10.0.0.2","ttl":600}`
|
||||||
|
handler := updateRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPut, "/v1/zones/test.example.com/records/"+idStr, body, map[string]string{
|
||||||
|
"zone": "test.example.com",
|
||||||
|
"id": idStr,
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var record db.Record
|
||||||
|
decodeJSON(t, rec, &record)
|
||||||
|
if record.Value != "10.0.0.2" {
|
||||||
|
t.Fatalf("value = %q, want %q", record.Value, "10.0.0.2")
|
||||||
|
}
|
||||||
|
if record.TTL != 600 {
|
||||||
|
t.Fatalf("ttl = %d, want 600", record.TTL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateRecordHandler_NotFound(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
body := `{"name":"www","type":"A","value":"10.0.0.1"}`
|
||||||
|
handler := updateRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPut, "/v1/zones/test.example.com/records/99999", body, map[string]string{
|
||||||
|
"zone": "test.example.com",
|
||||||
|
"id": "99999",
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateRecordHandler_InvalidID(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
body := `{"name":"www","type":"A","value":"10.0.0.1"}`
|
||||||
|
handler := updateRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPut, "/v1/zones/test.example.com/records/abc", body, map[string]string{
|
||||||
|
"zone": "test.example.com",
|
||||||
|
"id": "abc",
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateRecordHandler_MissingFields(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
created, err := database.CreateRecord("test.example.com", "www", "A", "10.0.0.1", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idStr := fmt.Sprintf("%d", created.ID)
|
||||||
|
|
||||||
|
// Missing name.
|
||||||
|
body := `{"type":"A","value":"10.0.0.1"}`
|
||||||
|
handler := updateRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodPut, "/v1/zones/test.example.com/records/"+idStr, body, map[string]string{
|
||||||
|
"zone": "test.example.com",
|
||||||
|
"id": idStr,
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRecordHandler_Success(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
createTestZone(t, database)
|
||||||
|
|
||||||
|
created, err := database.CreateRecord("test.example.com", "www", "A", "10.0.0.1", 300)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idStr := fmt.Sprintf("%d", created.ID)
|
||||||
|
handler := deleteRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodDelete, "/v1/zones/test.example.com/records/"+idStr, "", map[string]string{
|
||||||
|
"zone": "test.example.com",
|
||||||
|
"id": idStr,
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNoContent {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify record is gone.
|
||||||
|
_, err = database.GetRecord(created.ID)
|
||||||
|
if err != db.ErrNotFound {
|
||||||
|
t.Fatalf("expected ErrNotFound after delete, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRecordHandler_NotFound(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
handler := deleteRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodDelete, "/v1/zones/test.example.com/records/99999", "", map[string]string{
|
||||||
|
"zone": "test.example.com",
|
||||||
|
"id": "99999",
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRecordHandler_InvalidID(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
|
||||||
|
handler := deleteRecordHandler(database)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := newChiRequest(http.MethodDelete, "/v1/zones/test.example.com/records/abc", "", map[string]string{
|
||||||
|
"zone": "test.example.com",
|
||||||
|
"id": "abc",
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Middleware tests ----
|
||||||
|
|
||||||
|
func TestRequireAdmin_WithAdminContext(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
called = true
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
handler := requireAdmin(inner)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
|
||||||
|
// Inject admin TokenInfo into context.
|
||||||
|
info := &mcdslauth.TokenInfo{
|
||||||
|
Username: "admin-user",
|
||||||
|
IsAdmin: true,
|
||||||
|
Roles: []string{"admin"},
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(req.Context(), tokenInfoKey, info)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
if !called {
|
||||||
|
t.Fatal("inner handler was not called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequireAdmin_WithNonAdminContext(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
called = true
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
handler := requireAdmin(inner)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
|
||||||
|
// Inject non-admin TokenInfo into context.
|
||||||
|
info := &mcdslauth.TokenInfo{
|
||||||
|
Username: "regular-user",
|
||||||
|
IsAdmin: false,
|
||||||
|
Roles: []string{"viewer"},
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(req.Context(), tokenInfoKey, info)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusForbidden {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
if called {
|
||||||
|
t.Fatal("inner handler should not have been called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequireAdmin_NoTokenInfo(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
called = true
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
handler := requireAdmin(inner)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusForbidden {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
if called {
|
||||||
|
t.Fatal("inner handler should not have been called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractBearerToken(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
header string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"valid bearer", "Bearer abc123", "abc123"},
|
||||||
|
{"empty header", "", ""},
|
||||||
|
{"no prefix", "abc123", ""},
|
||||||
|
{"basic auth", "Basic abc123", ""},
|
||||||
|
{"bearer with spaces", "Bearer token-with-space ", "token-with-space"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
if tt.header != "" {
|
||||||
|
r.Header.Set("Authorization", tt.header)
|
||||||
|
}
|
||||||
|
got := extractBearerToken(r)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("extractBearerToken(%q) = %q, want %q", tt.header, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenInfoFromContext(t *testing.T) {
|
||||||
|
// No token info in context.
|
||||||
|
ctx := context.Background()
|
||||||
|
if info := tokenInfoFromContext(ctx); info != nil {
|
||||||
|
t.Fatal("expected nil, got token info")
|
||||||
|
}
|
||||||
|
|
||||||
|
// With token info.
|
||||||
|
expected := &mcdslauth.TokenInfo{Username: "testuser", IsAdmin: true}
|
||||||
|
ctx = context.WithValue(ctx, tokenInfoKey, expected)
|
||||||
|
got := tokenInfoFromContext(ctx)
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("expected token info, got nil")
|
||||||
|
}
|
||||||
|
if got.Username != expected.Username {
|
||||||
|
t.Fatalf("username = %q, want %q", got.Username, expected.Username)
|
||||||
|
}
|
||||||
|
if !got.IsAdmin {
|
||||||
|
t.Fatal("expected IsAdmin to be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- writeJSON / writeError tests ----
|
||||||
|
|
||||||
|
func TestWriteJSON(t *testing.T) {
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
writeJSON(rec, http.StatusOK, map[string]string{"key": "value"})
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
if ct := rec.Header().Get("Content-Type"); ct != "application/json" {
|
||||||
|
t.Fatalf("content-type = %q, want %q", ct, "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string]string
|
||||||
|
decodeJSON(t, rec, &resp)
|
||||||
|
if resp["key"] != "value" {
|
||||||
|
t.Fatalf("got key=%q, want %q", resp["key"], "value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteError(t *testing.T) {
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
writeError(rec, http.StatusBadRequest, "bad input")
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string]string
|
||||||
|
decodeJSON(t, rec, &resp)
|
||||||
|
if resp["error"] != "bad input" {
|
||||||
|
t.Fatalf("got error=%q, want %q", resp["error"], "bad input")
|
||||||
|
}
|
||||||
|
}
|
||||||
96
internal/server/middleware.go
Normal file
96
internal/server/middleware.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
174
internal/server/records.go
Normal file
174
internal/server/records.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
71
internal/server/routes.go
Normal file
71
internal/server/routes.go
Normal file
@@ -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})
|
||||||
|
}
|
||||||
|
|
||||||
163
internal/server/zones.go
Normal file
163
internal/server/zones.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
proto/mcns/v1/admin.proto
Normal file
16
proto/mcns/v1/admin.proto
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package mcns.v1;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/mcns/gen/mcns/v1;mcnsv1";
|
||||||
|
|
||||||
|
// AdminService exposes server health and administrative operations.
|
||||||
|
service AdminService {
|
||||||
|
rpc Health(HealthRequest) returns (HealthResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message HealthRequest {}
|
||||||
|
|
||||||
|
message HealthResponse {
|
||||||
|
string status = 1;
|
||||||
|
}
|
||||||
28
proto/mcns/v1/auth.proto
Normal file
28
proto/mcns/v1/auth.proto
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package mcns.v1;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/mcns/gen/mcns/v1;mcnsv1";
|
||||||
|
|
||||||
|
// AuthService handles authentication by delegating to MCIAS.
|
||||||
|
service AuthService {
|
||||||
|
rpc Login(LoginRequest) returns (LoginResponse);
|
||||||
|
rpc Logout(LogoutRequest) returns (LogoutResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginRequest {
|
||||||
|
string username = 1;
|
||||||
|
string password = 2;
|
||||||
|
// TOTP code for two-factor authentication, if enabled on the account.
|
||||||
|
string totp_code = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginResponse {
|
||||||
|
string token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LogoutRequest {
|
||||||
|
string token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LogoutResponse {}
|
||||||
68
proto/mcns/v1/record.proto
Normal file
68
proto/mcns/v1/record.proto
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package mcns.v1;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/mcns/gen/mcns/v1;mcnsv1";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
// RecordService manages DNS records within zones.
|
||||||
|
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;
|
||||||
|
// Zone name this record belongs to (e.g. "example.com.").
|
||||||
|
string zone = 2;
|
||||||
|
string name = 3;
|
||||||
|
// DNS record type (A, AAAA, CNAME, MX, TXT, etc.).
|
||||||
|
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;
|
||||||
|
// Optional filter by record name.
|
||||||
|
string name = 2;
|
||||||
|
// Optional filter by record type (A, AAAA, CNAME, etc.).
|
||||||
|
string type = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListRecordsResponse {
|
||||||
|
repeated Record records = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateRecordRequest {
|
||||||
|
// Zone name the record will be created in; must reference an existing zone.
|
||||||
|
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 {}
|
||||||
66
proto/mcns/v1/zone.proto
Normal file
66
proto/mcns/v1/zone.proto
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package mcns.v1;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/mcns/gen/mcns/v1;mcnsv1";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
// ZoneService manages DNS zones and their SOA parameters.
|
||||||
|
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 {}
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user