Add per-checker remote address CLI flags

Register one -checker-<id>-remote-address flag per registered checker,
allowing operators to delegate a checker's observation collection to a
remote HTTP service at startup. When set, the CLI/config value wins
over any per-checker "endpoint" AdminOpt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nemunaire 2026-04-27 00:05:24 +07:00
commit 0ee552a35b
5 changed files with 74 additions and 10 deletions

View file

@ -287,6 +287,11 @@ func (app *App) initUsecases() {
app.store,
app.store,
)
if setter, ok := app.usecases.checkerEngine.(interface {
SetRemoteAddresses(map[string]string)
}); ok {
setter.SetRemoteAddresses(app.cfg.CheckerRemoteAddresses)
}
// Build the user-level gate so paused or long-inactive users do not
// get checked. The same user resolver is reused by the janitor for
// per-user retention overrides.

View file

@ -27,6 +27,7 @@ import (
"runtime"
"time"
"git.happydns.org/happyDomain/internal/checker"
"git.happydns.org/happyDomain/internal/storage"
"git.happydns.org/happyDomain/model"
)
@ -70,6 +71,20 @@ func declareFlags(o *happydns.Options) {
flag.Var(&stringSlice{&o.PluginsDirectories}, "plugins-directory", "Path to a directory containing checker plugins (.so files); may be repeated")
// One -checker-<id>-remote-address flag per registered checker. Checkers
// register themselves in init() of the blank-imported `checkers` package,
// so by the time declareFlags runs the registry is fully populated.
if o.CheckerRemoteAddresses == nil {
o.CheckerRemoteAddresses = map[string]string{}
}
for id := range checker.GetCheckers() {
flag.Var(
&mapEntry{Map: &o.CheckerRemoteAddresses, Key: id},
fmt.Sprintf("checker-%s-remote-address", id),
fmt.Sprintf("URL of a remote HTTP service that should run the %q checker (overrides any per-checker endpoint AdminOpt)", id),
)
}
// Others flags are declared in some other files likes sources, storages, ... when they need specials configurations
}

View file

@ -46,6 +46,29 @@ func (s *stringSlice) Set(value string) error {
return nil
}
// mapEntry is a flag.Value that writes the flag value into a map under a
// preset key. Used to register one flag per checker writing into a shared
// map[string]string on Options.
type mapEntry struct {
Map *map[string]string
Key string
}
func (m *mapEntry) String() string {
if m.Map == nil || *m.Map == nil {
return ""
}
return (*m.Map)[m.Key]
}
func (m *mapEntry) Set(value string) error {
if *m.Map == nil {
*m.Map = map[string]string{}
}
(*m.Map)[m.Key] = value
return nil
}
type JWTSecretKey struct {
Secret *[]byte
}

View file

@ -34,14 +34,23 @@ import (
// checkerEngine implements the happydns.CheckerEngine interface.
type checkerEngine struct {
optionsUC *CheckerOptionsUsecase
evalStore CheckEvaluationStorage
execStore ExecutionStorage
snapStore ObservationSnapshotStorage
cacheStore ObservationCacheStorage
entryStore DiscoveryEntryStorage
obsRefStore DiscoveryObservationStorage
relatedLookup checkerPkg.RelatedObservationLookup
optionsUC *CheckerOptionsUsecase
evalStore CheckEvaluationStorage
execStore ExecutionStorage
snapStore ObservationSnapshotStorage
cacheStore ObservationCacheStorage
entryStore DiscoveryEntryStorage
obsRefStore DiscoveryObservationStorage
relatedLookup checkerPkg.RelatedObservationLookup
remoteAddresses map[string]string
}
// SetRemoteAddresses installs a checker-ID -> remote HTTP endpoint map. When
// a non-empty entry exists for a checker, runPipeline routes its observation
// collection through the remote service instead of the local provider, and
// takes precedence over any per-checker "endpoint" AdminOpt.
func (e *checkerEngine) SetRemoteAddresses(addrs map[string]string) {
e.remoteAddresses = addrs
}
// NewCheckerEngine creates a new CheckerEngine implementation. Passing nil
@ -189,8 +198,14 @@ func (e *checkerEngine) runPipeline(ctx context.Context, def *happydns.CheckerDe
obsCtx.SetRelatedLookup(def.ID, e.relatedLookup)
}
// If an endpoint is configured, override observation providers with HTTP transport.
if endpoint, ok := mergedOpts["endpoint"].(string); ok && endpoint != "" {
// If an endpoint is configured, override observation providers with HTTP
// transport. The CLI/config -checker-<id>-remote-address value (if set)
// wins over the per-checker "endpoint" AdminOpt.
endpoint, _ := mergedOpts["endpoint"].(string)
if cli, ok := e.remoteAddresses[def.ID]; ok && cli != "" {
endpoint = cli
}
if endpoint != "" {
for _, key := range def.ObservationKeys {
obsCtx.SetProviderOverride(key, checkerPkg.NewHTTPObservationProvider(key, endpoint))
}

View file

@ -134,6 +134,12 @@ type Options struct {
// PluginsDirectories lists filesystem paths scanned at startup for
// checker plugins (.so files).
PluginsDirectories []string
// CheckerRemoteAddresses maps a checker ID to the URL of a remote HTTP
// service that should run that checker's observation collection. When
// set for a given checker, this CLI/config value takes precedence over
// any per-checker "endpoint" AdminOpt.
CheckerRemoteAddresses map[string]string
}
// GetBaseURL returns the full url to the absolute ExternalURL, including BaseURL.