~bigbes/sr-ht-compare

aca0674ecc05078029322fd6343378768f3bbc40 — bigbes 9 days ago 07736d9
instconf: the origin ladder and the one-pass config check

validateConfig kept its shape and lost its body. instconf.Require is the
same decision — every missing key in one record, one exit, so an operator
edits config.ini once instead of restarting per gap — and firstConfigured
was this service's copy of the four-key API-origin ladder core-go panics
at the end of. NeedAny is handed APIOriginKeys() rather than the four
names written out, so the startup check and InternalAPIOrigin cannot
drift apart the way two literal lists eventually do.
1 files changed, 27 insertions(+), 38 deletions(-)

M cmd/comparesrht/main.go
M cmd/comparesrht/main.go => cmd/comparesrht/main.go +27 -38
@@ 25,7 25,7 @@
package main

import (
	"fmt"
	"errors"
	"log/slog"
	"os"
	"strings"


@@ 37,6 37,7 @@ import (
	"go.bigb.es/auxilia/scribe"
	"sourcecraft.dev/bigbes/sr-ht-core/config"
	coreserver "sourcecraft.dev/bigbes/sr-ht-core/server"
	"sourcecraft.dev/bigbes/sr-ht-ecore/instconf"
	"sourcecraft.dev/bigbes/sr-ht-ecore/logging"
	"sourcecraft.dev/bigbes/sr-ht-ecore/login"



@@ 173,49 174,37 @@ func main() {
// server.New), which would otherwise fatal with a terse message on a missing
// network-key or webhook key.
func validateConfig(conf ini.File) string {
	var missing []string
	require := func(section, key string) {
		if v, ok := conf.Get(section, key); !ok || strings.TrimSpace(v) == "" {
			missing = append(missing, fmt.Sprintf("[%s] %s", section, key))
		}
	}

	require("sr.ht", "network-key")    // crypto: unified-login cookie fernet key
	require("webhooks", "private-key") // crypto: webhook signing key (shared)
	require("git.sr.ht", "repos")      // bare repository root on disk
	require("meta.sr.ht", "origin")    // login/logout links in the nav
	require("compare.sr.ht", "origin") // our own external origin

	// git.sr.ht needs at least one internal API origin candidate; without it
	// config.GetAPI panics on the first authorization request.
	apiOrigin := firstConfigured(conf, "git.sr.ht",
		"api-internal-origin", "internal-origin", "api-origin", "origin")
	if apiOrigin == "" {
		missing = append(missing,
			"[git.sr.ht] one of api-internal-origin, internal-origin, api-origin, origin")
	}

	if len(missing) > 0 {
	err := instconf.Require(conf,
		instconf.Need("sr.ht", "network-key"),    // crypto: unified-login cookie fernet key
		instconf.Need("webhooks", "private-key"), // crypto: webhook signing key (shared)
		instconf.Need("git.sr.ht", "repos"),      // bare repository root on disk
		instconf.Need("meta.sr.ht", "origin"),    // login/logout links in the nav
		instconf.Need(service, "origin"),         // our own external origin

		// git.sr.ht needs at least one internal API origin candidate; without
		// it config.GetAPI panics on the first authorization request. The
		// ladder is asked for by name rather than written out, so this check
		// and the lookup below cannot come to disagree about it.
		instconf.NeedAny("git.sr.ht", instconf.APIOriginKeys()...),
	)
	if err != nil {
		// One record and one exit, deliberately: an operator fixing a config
		// wants every gap in front of them at once, and a line per missing key
		// is a restart per missing key. The keys go in as a slice attribute
		// rather than a joined string so the structured sinks keep them as a
		// is a restart per missing key. Strings() keeps them a slice attribute
		// rather than a joined string, so the structured sinks keep them as a
		// list — the tint handler still renders them on one line.
		slog.Error("incomplete configuration", "missing", missing)
		var missing *instconf.MissingKeysError
		if errors.As(err, &missing) {
			slog.Error("incomplete configuration", "missing", missing.Strings())
		} else {
			slog.Error("incomplete configuration", scribe.Err(err))
		}
		os.Exit(1)
	}
	return apiOrigin
}

// firstConfigured returns the value of the first present, non-empty key in
// section, or "" if none are set.
func firstConfigured(conf ini.File, section string, keys ...string) string {
	for _, k := range keys {
		if v, ok := conf.Get(section, k); ok && strings.TrimSpace(v) != "" {
			return v
		}
	}
	return ""
	// Cannot report false: NeedAny above walked the same ladder.
	apiOrigin, _ := instconf.InternalAPIOrigin(conf, "git.sr.ht")
	return apiOrigin
}

// resolveBind reconstructs the primary bind address server.New will use, for