Files
fancy-dns/internal/console/console.go
T
mats 66f36cfac6
build / build-push (push) Successful in 1m11s
more stuff
2026-10-16 14:49:50 +02:00

128 lines
4.0 KiB
Go

// Package console is the editing and viewing half of fancy-dns, split from the
// resolver so that it can run somewhere else. It owns authentication and the
// web UI, writes ClientGroup, BlockList and DNSZone objects to the cluster, and
// forwards everything the resolver owns (statistics, the query log, the cache)
// to the resolver's admin API.
//
// The resolver never calls the console and the console is never on the DNS
// path: stopping it cannot affect a query.
package console
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/api"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/config"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/kubeedit"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/web"
)
// Options configure a Console.
type Options struct {
Config *config.Store
Runtime *Runtime
Editor *kubeedit.Editor
Logger *slog.Logger
Version string
}
// Console serves the UI and the edit API.
type Console struct {
opt Options
log *slog.Logger
api *api.Server
started time.Time
}
// New builds a Console.
func New(opt Options) (*Console, error) {
if opt.Config == nil || opt.Runtime == nil || opt.Editor == nil {
return nil, errors.New("console: config, runtime and editor are required")
}
if opt.Logger == nil {
opt.Logger = slog.Default()
}
c := &Console{opt: opt, log: opt.Logger.With("component", "console"), started: time.Now()}
srv, err := api.New(c.hooks())
if err != nil {
return nil, err
}
c.api = srv
return c, nil
}
// actor names who is making a request, for the object annotation.
func actor(ctx context.Context) string {
if p, ok := api.PrincipalFrom(ctx); ok && p.Name != "" {
return p.Name
}
return ""
}
// Handler is the whole HTTP surface: the console's own routes, the resolver's
// API behind the same authentication, and the single-page app.
func (c *Console) Handler() (http.Handler, error) {
cfg := c.opt.Config.Get()
base := strings.TrimSuffix(cfg.API.BasePath, "/")
own := c.api.Handler()
mux := http.NewServeMux()
// Handled by the shared API server: authentication, the identity of the
// caller, and the edits whose hooks write to the cluster.
for _, p := range []string{
base + "/auth/",
"GET " + base + "/whoami",
"GET " + base + "/links",
"GET " + base + "/healthz",
"PUT " + base + "/clients",
"POST " + base + "/zones/records",
"PUT " + base + "/zones/records",
"DELETE " + base + "/zones/records",
"POST " + base + "/zones/delegations",
"DELETE " + base + "/zones/delegations",
"DELETE " + base + "/zones",
"POST " + base + "/blocklists/toggle",
"GET " + base + "/address-overrides",
"PUT " + base + "/address-overrides",
"DELETE " + base + "/address-overrides/{domain}",
"POST " + base + "/domains",
"DELETE " + base + "/domains/{name...}",
} {
mux.Handle(p, own)
}
// Reads that combine the cluster's objects with the resolver's view.
mux.Handle("GET "+base+"/zones", c.api.Protect(http.HandlerFunc(c.handleZones), false))
mux.Handle("GET "+base+"/clients", c.api.Protect(http.HandlerFunc(c.handleClients), false))
mux.Handle("POST "+base+"/clients/validate", c.api.Protect(http.HandlerFunc(c.handleClientsValidate), false))
mux.Handle("GET "+base+"/blocklist-sources", c.api.Protect(http.HandlerFunc(c.handleListSources), false))
mux.Handle("PUT "+base+"/blocklist-sources/{name}", c.api.Protect(http.HandlerFunc(c.handleListSourcePut), true))
mux.Handle("DELETE "+base+"/blocklist-sources/{name}", c.api.Protect(http.HandlerFunc(c.handleListSourceDelete), true))
// Everything else under the API belongs to the resolver.
proxy := c.opt.Runtime.Proxy()
read, write := c.api.Protect(proxy, false), c.api.Protect(proxy, true)
mux.Handle(base+"/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet || r.Method == http.MethodHead {
read.ServeHTTP(w, r)
return
}
write.ServeHTTP(w, r)
}))
if cfg.API.UI {
ui, err := web.Handler("")
if err != nil {
return nil, fmt.Errorf("web console: %w", err)
}
mux.Handle("/", ui)
}
return mux, nil
}