From d847c71a509dde358a8772c5ba15e756399734d3 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 23 Apr 2026 10:06:48 +0700 Subject: [PATCH] checker: let CheckRule.Evaluate return per-subject CheckStates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rules that iterate over multiple elements (certificates, CAA records, nameservers, …) previously had to squash per-element results into a single concatenated message. Evaluate now returns []CheckState and CheckState carries an opaque Subject, so each element gets its own structured state. The server injects a StatusUnknown placeholder when a rule returns nothing, to avoid silently dropping the rule. --- checker/server.go | 16 ++++++++++++---- checker/server_test.go | 4 ++-- checker/types.go | 19 ++++++++++++++++--- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/checker/server.go b/checker/server.go index 8b2eb31..444c118 100644 --- a/checker/server.go +++ b/checker/server.go @@ -291,11 +291,19 @@ func (s *Server) handleEvaluate(w http.ResponseWriter, r *http.Request) { continue } } - state := rule.Evaluate(r.Context(), obs, req.Options) - if state.Code == "" { - state.Code = rule.Name() + ruleStates := rule.Evaluate(r.Context(), obs, req.Options) + if len(ruleStates) == 0 { + ruleStates = []CheckState{{ + Status: StatusUnknown, + Message: fmt.Sprintf("rule %q returned no state", rule.Name()), + }} + } + for _, state := range ruleStates { + if state.Code == "" { + state.Code = rule.Name() + } + states = append(states, state) } - states = append(states, state) } writeJSON(w, http.StatusOK, ExternalEvaluateResponse{States: states}) diff --git a/checker/server_test.go b/checker/server_test.go index 6a228b6..23ac446 100644 --- a/checker/server_test.go +++ b/checker/server_test.go @@ -65,8 +65,8 @@ type dummyRule struct { func (r *dummyRule) Name() string { return r.name } func (r *dummyRule) Description() string { return r.desc } -func (r *dummyRule) Evaluate(ctx context.Context, obs ObservationGetter, opts CheckerOptions) CheckState { - return CheckState{Status: StatusOK, Message: r.name + " passed"} +func (r *dummyRule) Evaluate(ctx context.Context, obs ObservationGetter, opts CheckerOptions) []CheckState { + return []CheckState{{Status: StatusOK, Message: r.name + " passed"}} } // --- helpers --- diff --git a/checker/types.go b/checker/types.go index 9099a2f..2600987 100644 --- a/checker/types.go +++ b/checker/types.go @@ -182,11 +182,15 @@ func (s Status) String() string { } } -// CheckState is the result of evaluating a single rule. +// CheckState is the result of evaluating a single rule on a single subject. +// Subject is opaque to the SDK: producers and consumers agree on its shape +// (a hostname, a record key, a serial, …). Leave Subject empty for rules +// that produce a single, global result. type CheckState struct { Status Status `json:"status"` Message string `json:"message"` Code string `json:"code,omitempty"` + Subject string `json:"subject,omitempty"` Meta map[string]any `json:"meta,omitempty"` } @@ -222,11 +226,20 @@ type CheckRuleInfo struct { Options *CheckerOptionsDocumentation `json:"options,omitempty"` } -// CheckRule evaluates observations and produces a CheckState. +// CheckRule evaluates observations and produces one or more CheckStates. +// +// Evaluate returns a slice so a rule iterating over multiple elements can +// emit one state per subject (each carrying CheckState.Subject) without +// squashing them into a single concatenated message. +// +// Evaluate must not return a nil or empty slice: callers expect at least +// one state per rule. When a rule finds nothing to evaluate, return a +// single CheckState with an appropriate status (typically StatusInfo or +// StatusOK) describing that fact. type CheckRule interface { Name() string Description() string - Evaluate(ctx context.Context, obs ObservationGetter, opts CheckerOptions) CheckState + Evaluate(ctx context.Context, obs ObservationGetter, opts CheckerOptions) []CheckState } // CheckRuleWithOptions is an optional interface that rules can implement