Expose one rule per source with rule-scoped options
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing

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.
This commit is contained in:
nemunaire 2026-05-19 22:10:32 +08:00
commit ce59a976d5
5 changed files with 49 additions and 51 deletions

View file

@ -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()

View file

@ -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",

View file

@ -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:

2
go.mod
View file

@ -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
)

4
go.sum
View file

@ -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=