// Command fancy-dns-console is the editing half of fancy-dns: the web UI and // the API behind it, writing ClientGroup, BlockList and DNSZone objects to // Kubernetes and forwarding everything else to a running resolver. // // It can run in the cluster or on a workstation (with a kubeconfig); the // resolver it manages need not be in the same place. package main import ( "context" "errors" "flag" "fmt" "log/slog" "net" "net/http" "os" "os/signal" "strings" "syscall" "time" "git.k8s-git-k6n.net/mats/fancy-dns/internal/app" "git.k8s-git-k6n.net/mats/fancy-dns/internal/config" "git.k8s-git-k6n.net/mats/fancy-dns/internal/console" "git.k8s-git-k6n.net/mats/fancy-dns/internal/kubeedit" "git.k8s-git-k6n.net/mats/fancy-dns/internal/kubesource" ) func main() { // `fancy-dns-console migrate ...` converts an existing ConfigMap-and-YAML // deployment into cluster objects; see migrate.go. if len(os.Args) > 1 && os.Args[1] == "migrate" { if err := migrateMain(os.Args[2:], os.Stdout, os.Stderr); err != nil { if !errors.Is(err, flag.ErrHelp) { fmt.Fprintln(os.Stderr, "fancy-dns-console migrate:", err) } os.Exit(1) } return } var ( configPath = flag.String("config", envOr("FANCYDNS_CONSOLE_CONFIG", "/etc/fancy-dns/console.yaml"), "console configuration (api: listen, auth, ui)") runtimeURL = flag.String("runtime-url", os.Getenv("FANCYDNS_RUNTIME_URL"), "admin API of the resolver, e.g. http://fancy-dns.dns:9053") kubeconfig = flag.String("kubeconfig", os.Getenv("FANCYDNS_KUBECONFIG"), "kubeconfig (default: in-cluster service account, then $KUBECONFIG, then ~/.kube/config)") kubeContext = flag.String("kube-context", os.Getenv("FANCYDNS_KUBE_CONTEXT"), "kubeconfig context") kubeNS = flag.String("kube-namespace", os.Getenv("FANCYDNS_KUBE_NAMESPACE"), "namespace the objects live in (default: the pod's own)") showVersion = flag.Bool("version", false, "print the version and exit") checkOnly = flag.Bool("check", false, "validate the configuration and exit") ) flag.Parse() if *showVersion { fmt.Println("fancy-dns-console", app.Version) return } if err := run(*configPath, *runtimeURL, *kubeconfig, *kubeContext, *kubeNS, *checkOnly); err != nil { fmt.Fprintln(os.Stderr, "fancy-dns-console:", err) os.Exit(1) } } func run(configPath, runtimeURL, kubeconfig, kubeContext, ns string, checkOnly bool) error { cfg, err := config.Load(configPath) if err != nil { if strings.Contains(err.Error(), "blocklist is enabled") { err = fmt.Errorf("%w (the console has no blocklists of its own: put \"blocklist: {enabled: false}\" in its config)", err) } return err } if runtimeURL == "" { return errors.New("-runtime-url (or FANCYDNS_RUNTIME_URL) is required") } rt, err := console.NewRuntime(runtimeURL, os.Getenv("FANCYDNS_RUNTIME_KEY")) if err != nil { return err } if ns == "" { ns = kubesource.Namespace() } if ns == "" { return errors.New("-kube-namespace is required when not running in a pod") } client, from, err := kubesource.NewClient(kubesource.ClientOptions{Kubeconfig: kubeconfig, Context: kubeContext}) if err != nil { return err } level := slog.LevelInfo if cfg.Log.Level == "debug" { level = slog.LevelDebug } log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})).With("service", "fancy-dns-console", "version", app.Version) c, err := console.New(console.Options{ Config: config.NewStore(cfg, configPath), Runtime: rt, Editor: kubeedit.New(client, ns), Logger: log, Version: app.Version, }) if err != nil { return err } handler, err := c.Handler() if err != nil { return err } if checkOnly { fmt.Println("configuration is valid:", configPath) return nil } if len(cfg.API.TLSListen) > 0 { log.Warn("api.tls_listen is ignored by the console; terminate TLS at an ingress or proxy") } if len(cfg.API.Listen) == 0 { return errors.New("api.listen is empty: nowhere to serve the console") } srv := &http.Server{Handler: handler, ReadHeaderTimeout: 15 * time.Second, IdleTimeout: 120 * time.Second} errc := make(chan error, len(cfg.API.Listen)) for _, addr := range cfg.API.Listen { ln, err := net.Listen("tcp", addr) if err != nil { return fmt.Errorf("bind %s: %w", addr, err) } log.Info("console listening", "addr", ln.Addr().String(), "namespace", ns, "kubernetes", from, "resolver", runtimeURL) go func() { errc <- srv.Serve(ln) }() } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() select { case <-ctx.Done(): case err := <-errc: if !errors.Is(err, http.ErrServerClosed) { return err } } sctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return srv.Shutdown(sctx) } func envOr(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def }