From ce59a976d54922874f4ce114799e458a81e2183b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 19 May 2026 22:10:32 +0800 Subject: [PATCH] Expose one rule per source with rule-scoped options Each registered Source now becomes its own CheckRule (name = source ID) implementing CheckRuleWithOptions, so the host can toggle blacklists individually and the per-source option fields show up under the rule that owns them instead of one flat global option list. Collect honours the host's per-rule enable map (via the SDK's RuleEnabled context helper) and skips the network call for disabled sources entirely, not just their evaluation. --- checker/collect.go | 5 +++ checker/definition.go | 12 ++----- checker/rule.go | 81 +++++++++++++++++++++---------------------- go.mod | 2 +- go.sum | 4 +-- 5 files changed, 51 insertions(+), 53 deletions(-) diff --git a/checker/collect.go b/checker/collect.go index 446a195..076f98e 100644 --- a/checker/collect.go +++ b/checker/collect.go @@ -38,6 +38,11 @@ func (p *blacklistProvider) Collect(ctx context.Context, opts sdk.CheckerOptions var wg sync.WaitGroup for i, s := range sources { + // The host disables a source by disabling its rule (rule name == source ID). + // Skip the network call entirely; Evaluate is short-circuited host-side. + if !sdk.RuleEnabled(ctx, s.ID()) { + continue + } wg.Add(1) go func(i int, s Source) { defer wg.Done() diff --git a/checker/definition.go b/checker/definition.go index 14c67e9..fcbd9aa 100644 --- a/checker/definition.go +++ b/checker/definition.go @@ -9,10 +9,9 @@ import ( // Version is overridden at link time by the standalone or plugin entrypoints. var Version = "built-in" -// Definition assembles the checker definition by aggregating each -// registered Source's options into the SDK's audience-grouped layout. -// Adding a source automatically adds its option fields here: no edit -// to this file needed. +// Definition assembles the checker definition. Per-source option fields +// live on each per-source rule (CheckRuleWithOptions); the global Options +// only carries the shared domain target. func Definition() *sdk.CheckerDefinition { opts := sdk.CheckerOptionsDocumentation{ DomainOpts: []sdk.CheckerOptionDocumentation{ @@ -23,11 +22,6 @@ func Definition() *sdk.CheckerDefinition { }, }, } - for _, s := range Sources() { - o := s.Options() - opts.AdminOpts = append(opts.AdminOpts, o.Admin...) - opts.UserOpts = append(opts.UserOpts, o.User...) - } return &sdk.CheckerDefinition{ ID: "blacklist", diff --git a/checker/rule.go b/checker/rule.go index 72d5c38..d904836 100644 --- a/checker/rule.go +++ b/checker/rule.go @@ -7,23 +7,39 @@ import ( sdk "git.happydns.org/checker-sdk-go/checker" ) -// Rules returns the rule set surfaced to happyDomain. After the -// registry refactor we expose a single, generic rule that emits one -// CheckState per source result: the per-source verdict lives in -// CheckState.Subject (the source name) and CheckState.Code carries the -// canonical hit / clean / disabled / error flavour. +// Rules returns one rule per registered Source. The host can enable or +// disable each independently, and each rule owns the option fields its +// Source contributes (CheckRuleWithOptions). The rule name is the +// Source ID; downstream a disabled rule both skips evaluation and +// skips the underlying network call in Collect. func Rules() []sdk.CheckRule { - return []sdk.CheckRule{&sourceRule{}} + srcs := Sources() + rules := make([]sdk.CheckRule, 0, len(srcs)) + for _, s := range srcs { + rules = append(rules, &sourceRule{src: s}) + } + return rules } -type sourceRule struct{} - -func (*sourceRule) Name() string { return "source_listed" } -func (*sourceRule) Description() string { - return "Emits one state per reputation source: Critical/Warning when the source flags the domain, OK when clean, Info when the source is disabled, and Warning on transient query errors." +type sourceRule struct { + src Source } -func (*sourceRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { +func (r *sourceRule) Name() string { return r.src.ID() } + +func (r *sourceRule) Description() string { + return fmt.Sprintf("%s reputation lookup. Emits Critical/Warning when the source flags the domain, OK when clean, Info when the source is disabled or not configured, and Warning on transient query errors.", r.src.Name()) +} + +func (r *sourceRule) Options() sdk.CheckerOptionsDocumentation { + o := r.src.Options() + return sdk.CheckerOptionsDocumentation{ + AdminOpts: o.Admin, + UserOpts: o.User, + } +} + +func (r *sourceRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { var data BlacklistData if err := obs.Get(ctx, ObservationKeyBlacklist, &data); err != nil { return []sdk.CheckState{{ @@ -33,25 +49,19 @@ func (*sourceRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts }} } - if len(data.Results) == 0 { - return []sdk.CheckState{{ - Status: sdk.StatusInfo, Message: "No reputation sources registered.", - Code: "blacklist_no_sources", - }} - } - - byID := make(map[string]Source, len(Sources())) - for _, s := range Sources() { - byID[s.ID()] = s - } - - out := make([]sdk.CheckState, 0, len(data.Results)) - for _, r := range data.Results { - src, ok := byID[r.SourceID] - if !ok { - src = unknownSource{} + var out []sdk.CheckState + for _, res := range data.Results { + if res.SourceID != r.src.ID() { + continue } - out = append(out, evaluateOne(r, src)) + out = append(out, evaluateOne(res, r.src)) + } + if len(out) == 0 { + return []sdk.CheckState{{ + Status: sdk.StatusUnknown, + Message: r.src.Name() + ": no result (source skipped or disabled).", + Code: "source_disabled", + }} } return out } @@ -105,17 +115,6 @@ func evaluateOne(r SourceResult, src Source) sdk.CheckState { } } -// unknownSource is a sentinel used when a SourceResult references a source ID -// that is no longer in the registry. Evaluate always returns (false, ""). -type unknownSource struct{} - -func (unknownSource) ID() string { return "" } -func (unknownSource) Name() string { return "unknown" } -func (unknownSource) Options() SourceOptions { return SourceOptions{} } -func (unknownSource) Query(_ context.Context, _, _ string, _ sdk.CheckerOptions) []SourceResult { return nil } -func (unknownSource) Diagnose(_ SourceResult) Diagnosis { return Diagnosis{} } -func (unknownSource) Evaluate(_ SourceResult) (bool, string) { return false, "" } - func severityToStatus(sev string) sdk.Status { switch sev { case SeverityCrit: diff --git a/go.mod b/go.mod index 042aaf7..cc71d5d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module git.happydns.org/checker-blacklist go 1.25.0 require ( - git.happydns.org/checker-sdk-go v1.5.0 + git.happydns.org/checker-sdk-go v1.8.0 golang.org/x/net v0.34.0 ) diff --git a/go.sum b/go.sum index 5631c56..d14c68e 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -git.happydns.org/checker-sdk-go v1.5.0 h1:5uD5Cm6xJ+lwnhbJ09iCXGHbYS9zRh+Yh0NeBHkAPBY= -git.happydns.org/checker-sdk-go v1.5.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= +git.happydns.org/checker-sdk-go v1.8.0 h1:2lhcSc16rnCaszdQi1nerszb2c3fVh5XNS11pLrXuK4= +git.happydns.org/checker-sdk-go v1.8.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=