Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d387cd629b | |||
| f203b2e573 | |||
| c72558e266 | |||
| c1de9aca1c |
5 changed files with 405 additions and 4 deletions
52
checker/context.go
Normal file
52
checker/context.go
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright 2020-2026 The happyDomain Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type enabledRulesCtxKey struct{}
|
||||||
|
|
||||||
|
// WithEnabledRules returns a context carrying the host's per-rule enable map.
|
||||||
|
// The SDK server attaches it before calling ObservationProvider.Collect so
|
||||||
|
// providers can skip optional work (network calls, paid API hits, …) for
|
||||||
|
// rules the host has disabled. A nil map means "run everything".
|
||||||
|
func WithEnabledRules(ctx context.Context, enabled map[string]bool) context.Context {
|
||||||
|
if enabled == nil {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, enabledRulesCtxKey{}, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnabledRulesFromContext returns the enabled-rule map attached by
|
||||||
|
// WithEnabledRules, or nil if none. RuleEnabled is the usual access pattern.
|
||||||
|
func EnabledRulesFromContext(ctx context.Context) map[string]bool {
|
||||||
|
m, _ := ctx.Value(enabledRulesCtxKey{}).(map[string]bool)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuleEnabled reports whether ruleName is enabled given the host's map.
|
||||||
|
// Absent rules default to enabled (nil map or rule not in map), matching
|
||||||
|
// the SDK server's evaluate-side semantics.
|
||||||
|
func RuleEnabled(ctx context.Context, ruleName string) bool {
|
||||||
|
m := EnabledRulesFromContext(ctx)
|
||||||
|
if m == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
enabled, ok := m[ruleName]
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
37
checker/names.go
Normal file
37
checker/names.go
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright 2020-2026 The happyDomain Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// JoinRelative treats name as relative to origin, as happyDomain encodes
|
||||||
|
// service-embedded record owners and subdomains. An empty or "@" name
|
||||||
|
// resolves to the origin itself; an empty origin returns the trimmed name
|
||||||
|
// unchanged. A name already suffixed by origin is returned as-is so that
|
||||||
|
// absolute encodings round-trip safely. Trailing dots are stripped.
|
||||||
|
func JoinRelative(name, origin string) string {
|
||||||
|
origin = strings.TrimSuffix(origin, ".")
|
||||||
|
name = strings.TrimSuffix(name, ".")
|
||||||
|
if origin == "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
if name == "" || name == "@" {
|
||||||
|
return origin
|
||||||
|
}
|
||||||
|
if name == origin || strings.HasSuffix(name, "."+origin) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return name + "." + origin
|
||||||
|
}
|
||||||
|
|
@ -133,6 +133,7 @@ func New(provider checker.ObservationProvider) *Server {
|
||||||
s.definition = def
|
s.definition = def
|
||||||
s.definition.BuildRulesInfo()
|
s.definition.BuildRulesInfo()
|
||||||
s.mux.HandleFunc("GET /definition", s.handleDefinition)
|
s.mux.HandleFunc("GET /definition", s.handleDefinition)
|
||||||
|
s.mux.Handle("POST /definition", s.TrackWork(http.HandlerFunc(s.handlePrecheck)))
|
||||||
s.mux.Handle("POST /evaluate", s.TrackWork(http.HandlerFunc(s.handleEvaluate)))
|
s.mux.Handle("POST /evaluate", s.TrackWork(http.HandlerFunc(s.handleEvaluate)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -316,6 +317,51 @@ func (s *Server) handleDefinition(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, http.StatusOK, s.definition)
|
writeJSON(w, http.StatusOK, s.definition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handlePrecheck answers POST /definition: it returns the same
|
||||||
|
// definition body as GET /definition, plus a PrecheckFailures map
|
||||||
|
// listing rules whose prerequisites are unmet for the submitted
|
||||||
|
// options. Rules that do not implement checker.RulePrecheck, or whose
|
||||||
|
// Precheck returned nil, are omitted from that map.
|
||||||
|
func (s *Server) handlePrecheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req checker.RulePrecheckRequest
|
||||||
|
if r.ContentLength != 0 {
|
||||||
|
if err := json.NewDecoder(io.LimitReader(r.Body, maxRequestBodySize)).Decode(&req); err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
||||||
|
"error": fmt.Sprintf("invalid request body: %v", err),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failures := map[string]string{}
|
||||||
|
for _, rule := range s.definition.Rules {
|
||||||
|
pc, ok := rule.(checker.RulePrecheck)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := pc.Precheck(r.Context(), req.Options); err != nil {
|
||||||
|
failures[rule.Name()] = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp := checker.RulePrecheckResponse{
|
||||||
|
CheckerDefinition: s.definition,
|
||||||
|
PrecheckFailures: failures,
|
||||||
|
}
|
||||||
|
if en, ok := s.provider.(checker.CheckEnabler); ok {
|
||||||
|
eligible, reason, err := en.IsEligible(r.Context(), req.Options)
|
||||||
|
if err != nil {
|
||||||
|
// Eligibility undetermined: leave Eligible nil so the host fails
|
||||||
|
// open (shows the checker), but surface the error for diagnostics.
|
||||||
|
log.Printf("IsEligible failed: %v", err)
|
||||||
|
resp.EligibilityReason = err.Error()
|
||||||
|
} else {
|
||||||
|
resp.Eligible = &eligible
|
||||||
|
resp.EligibilityReason = reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleCollect(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleCollect(w http.ResponseWriter, r *http.Request) {
|
||||||
var req checker.ExternalCollectRequest
|
var req checker.ExternalCollectRequest
|
||||||
if err := json.NewDecoder(io.LimitReader(r.Body, maxRequestBodySize)).Decode(&req); err != nil {
|
if err := json.NewDecoder(io.LimitReader(r.Body, maxRequestBodySize)).Decode(&req); err != nil {
|
||||||
|
|
@ -325,7 +371,8 @@ func (s *Server) handleCollect(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := s.provider.Collect(r.Context(), req.Options)
|
ctx := checker.WithEnabledRules(r.Context(), req.EnabledRules)
|
||||||
|
data, err := s.provider.Collect(ctx, req.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, http.StatusInternalServerError, checker.ExternalCollectResponse{
|
writeJSON(w, http.StatusInternalServerError, checker.ExternalCollectResponse{
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
|
|
|
||||||
|
|
@ -667,3 +667,196 @@ func TestServer_NoDefinition_NoEvaluateEndpoint(t *testing.T) {
|
||||||
t.Error("POST /evaluate should not be available without CheckerDefinitionProvider")
|
t.Error("POST /evaluate should not be available without CheckerDefinitionProvider")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prereqRule implements RulePrecheck, failing when a named option is empty.
|
||||||
|
type prereqRule struct {
|
||||||
|
name string
|
||||||
|
optKey string
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *prereqRule) Name() string { return r.name }
|
||||||
|
func (r *prereqRule) Description() string { return "" }
|
||||||
|
func (r *prereqRule) Evaluate(ctx context.Context, obs checker.ObservationGetter, opts checker.CheckerOptions) []checker.CheckState {
|
||||||
|
return []checker.CheckState{{Status: checker.StatusOK}}
|
||||||
|
}
|
||||||
|
func (r *prereqRule) Precheck(ctx context.Context, opts checker.CheckerOptions) error {
|
||||||
|
if v, _ := opts[r.optKey].(string); v == "" {
|
||||||
|
return errors.New(r.msg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// enablerProvider is a minimal ObservationProvider + CheckerDefinitionProvider
|
||||||
|
// that also implements CheckEnabler, returning whatever isEligibleFn yields.
|
||||||
|
type enablerProvider struct {
|
||||||
|
key checker.ObservationKey
|
||||||
|
definition *checker.CheckerDefinition
|
||||||
|
isEligibleFn func(ctx context.Context, opts checker.CheckerOptions) (bool, string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *enablerProvider) Key() checker.ObservationKey { return p.key }
|
||||||
|
func (p *enablerProvider) Collect(ctx context.Context, opts checker.CheckerOptions) (any, error) {
|
||||||
|
return map[string]string{"result": "ok"}, nil
|
||||||
|
}
|
||||||
|
func (p *enablerProvider) Definition() *checker.CheckerDefinition { return p.definition }
|
||||||
|
func (p *enablerProvider) IsEligible(ctx context.Context, opts checker.CheckerOptions) (bool, string, error) {
|
||||||
|
return p.isEligibleFn(ctx, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_Precheck_Eligibility(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fn func(ctx context.Context, opts checker.CheckerOptions) (bool, string, error)
|
||||||
|
wantNil bool // expect Eligible == nil
|
||||||
|
wantElig bool // value of *Eligible when not nil
|
||||||
|
wantReason string // expected EligibilityReason
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "eligible true",
|
||||||
|
fn: func(context.Context, checker.CheckerOptions) (bool, string, error) { return true, "", nil },
|
||||||
|
wantElig: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "eligible false with reason",
|
||||||
|
fn: func(context.Context, checker.CheckerOptions) (bool, string, error) { return false, "not a reverse zone", nil },
|
||||||
|
wantElig: false,
|
||||||
|
wantReason: "not a reverse zone",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error fails open",
|
||||||
|
fn: func(context.Context, checker.CheckerOptions) (bool, string, error) { return false, "", errors.New("lookup timeout") },
|
||||||
|
wantNil: true,
|
||||||
|
wantReason: "lookup timeout",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
p := &enablerProvider{
|
||||||
|
key: "test",
|
||||||
|
definition: &checker.CheckerDefinition{ID: "test", Rules: []checker.CheckRule{}},
|
||||||
|
isEligibleFn: tc.fn,
|
||||||
|
}
|
||||||
|
srv := New(p)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
rec := doRequest(srv.Handler(), "POST", "/definition", checker.RulePrecheckRequest{Options: checker.CheckerOptions{}}, nil)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("POST /definition = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
var resp checker.RulePrecheckResponse
|
||||||
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
||||||
|
t.Fatalf("decode: %v", err)
|
||||||
|
}
|
||||||
|
if tc.wantNil {
|
||||||
|
if resp.Eligible != nil {
|
||||||
|
t.Errorf("Eligible = %v, want nil", *resp.Eligible)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if resp.Eligible == nil {
|
||||||
|
t.Fatalf("Eligible = nil, want %v", tc.wantElig)
|
||||||
|
}
|
||||||
|
if *resp.Eligible != tc.wantElig {
|
||||||
|
t.Errorf("Eligible = %v, want %v", *resp.Eligible, tc.wantElig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resp.EligibilityReason != tc.wantReason {
|
||||||
|
t.Errorf("EligibilityReason = %q, want %q", resp.EligibilityReason, tc.wantReason)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServer_Precheck_NoEnabler verifies that a provider not implementing
|
||||||
|
// CheckEnabler yields no eligibility fields (Eligible nil, reason empty).
|
||||||
|
func TestServer_Precheck_NoEnabler(t *testing.T) {
|
||||||
|
p := &testProvider{key: "test", definition: &checker.CheckerDefinition{ID: "test", Rules: []checker.CheckRule{}}}
|
||||||
|
srv := newTestServer(p)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
rec := doRequest(srv.Handler(), "POST", "/definition", checker.RulePrecheckRequest{Options: checker.CheckerOptions{}}, nil)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("POST /definition = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
if bytes.Contains(rec.Body.Bytes(), []byte("eligible")) {
|
||||||
|
t.Errorf("response leaked eligible field for non-enabler provider: %s", rec.Body.String())
|
||||||
|
}
|
||||||
|
var resp checker.RulePrecheckResponse
|
||||||
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
||||||
|
t.Fatalf("decode: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Eligible != nil {
|
||||||
|
t.Errorf("Eligible = %v, want nil for non-enabler provider", *resp.Eligible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_Precheck(t *testing.T) {
|
||||||
|
gated := &prereqRule{name: "gated", optKey: "api_key", msg: "missing API key"}
|
||||||
|
open := &dummyRule{name: "open", desc: "no prereq"}
|
||||||
|
p := &testProvider{
|
||||||
|
key: "test",
|
||||||
|
definition: &checker.CheckerDefinition{
|
||||||
|
ID: "test",
|
||||||
|
Rules: []checker.CheckRule{gated, open},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
srv := newTestServer(p)
|
||||||
|
defer srv.Close()
|
||||||
|
handler := srv.Handler()
|
||||||
|
|
||||||
|
// GET /definition stays static — no precheck information surfaces here.
|
||||||
|
rec := doRequest(handler, "GET", "/definition", nil, nil)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /definition = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
if bytes.Contains(rec.Body.Bytes(), []byte("precheck_failures")) {
|
||||||
|
t.Errorf("GET /definition leaked precheck_failures field: %s", rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /definition with empty opts: gated rule fails, open rule absent.
|
||||||
|
rec = doRequest(handler, "POST", "/definition", checker.RulePrecheckRequest{Options: checker.CheckerOptions{}}, nil)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("POST /definition (empty opts) = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
var resp checker.RulePrecheckResponse
|
||||||
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
||||||
|
t.Fatalf("decode POST /definition: %v", err)
|
||||||
|
}
|
||||||
|
if resp.CheckerDefinition == nil {
|
||||||
|
t.Fatalf("POST /definition response missing embedded CheckerDefinition")
|
||||||
|
}
|
||||||
|
if resp.ID != "test" {
|
||||||
|
t.Errorf("response ID = %q, want %q", resp.ID, "test")
|
||||||
|
}
|
||||||
|
if len(resp.RulesInfo) != 2 {
|
||||||
|
t.Errorf("response RulesInfo len = %d, want 2", len(resp.RulesInfo))
|
||||||
|
}
|
||||||
|
if got := resp.PrecheckFailures["gated"]; got != "missing API key" {
|
||||||
|
t.Errorf("PrecheckFailures[gated] = %q, want %q", got, "missing API key")
|
||||||
|
}
|
||||||
|
if _, ok := resp.PrecheckFailures["open"]; ok {
|
||||||
|
t.Errorf("PrecheckFailures[open] should be absent (no RulePrecheck impl), got %q", resp.PrecheckFailures["open"])
|
||||||
|
}
|
||||||
|
if len(resp.PrecheckFailures) != 1 {
|
||||||
|
t.Errorf("PrecheckFailures = %v, want exactly 1 entry", resp.PrecheckFailures)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /definition with sufficient opts: empty failure map.
|
||||||
|
rec = doRequest(handler, "POST", "/definition", checker.RulePrecheckRequest{
|
||||||
|
Options: checker.CheckerOptions{"api_key": "secret"},
|
||||||
|
}, nil)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("POST /definition (with opts) = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
resp = checker.RulePrecheckResponse{}
|
||||||
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
||||||
|
t.Fatalf("decode POST /definition: %v", err)
|
||||||
|
}
|
||||||
|
if resp.CheckerDefinition == nil || resp.ID != "test" {
|
||||||
|
t.Errorf("response missing definition: %+v", resp)
|
||||||
|
}
|
||||||
|
if len(resp.PrecheckFailures) != 0 {
|
||||||
|
t.Errorf("PrecheckFailures = %v, want empty when opts satisfy prereqs", resp.PrecheckFailures)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,71 @@ type CheckRuleWithOptions interface {
|
||||||
Options() CheckerOptionsDocumentation
|
Options() CheckerOptionsDocumentation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RulePrecheck is an optional interface a CheckRule can implement to
|
||||||
|
// declare whether the current options are sufficient for the rule to
|
||||||
|
// run. Return nil if runnable, or an error describing the missing
|
||||||
|
// prerequisite (for example "missing API key"). The host calls this via
|
||||||
|
// POST /definition to surface unavailable rules in the UI; it is never
|
||||||
|
// invoked from Collect, so rules that need to short-circuit at run time
|
||||||
|
// must keep their own self-guard.
|
||||||
|
type RulePrecheck interface {
|
||||||
|
CheckRule
|
||||||
|
Precheck(ctx context.Context, opts CheckerOptions) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckEnabler is an optional interface an ObservationProvider can implement
|
||||||
|
// to declare, from the actual target data, whether running this checker is
|
||||||
|
// meaningful at all.
|
||||||
|
//
|
||||||
|
// It complements the two existing gates:
|
||||||
|
// - CheckerAvailability is a static, registration-time scope/service-type
|
||||||
|
// filter; it never sees the target's data.
|
||||||
|
// - RulePrecheck is a per-rule, options-only check ("missing API key").
|
||||||
|
//
|
||||||
|
// CheckEnabler is whole-checker and data-driven. IsEligible receives the same
|
||||||
|
// CheckerOptions as Collect, including the autofilled domain_name / zone /
|
||||||
|
// service payloads (read them with GetOption), and may perform light I/O
|
||||||
|
// (e.g. a DNSKEY lookup) to decide.
|
||||||
|
//
|
||||||
|
// Return (true, "", nil) to run the checker, or (false, reason, nil) with a
|
||||||
|
// short human-readable reason ("not a reverse zone", "DNSSEC not enabled")
|
||||||
|
// to skip it. Return a non-nil error only when eligibility could not be
|
||||||
|
// determined (transient I/O failure); the host treats that as "unknown" and
|
||||||
|
// fails open (shows the checker) rather than as a definitive skip.
|
||||||
|
//
|
||||||
|
// Detect support with a type assertion: _, ok := provider.(CheckEnabler)
|
||||||
|
type CheckEnabler interface {
|
||||||
|
IsEligible(ctx context.Context, opts CheckerOptions) (eligible bool, reason string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RulePrecheckRequest is the body accepted by POST /definition.
|
||||||
|
type RulePrecheckRequest struct {
|
||||||
|
Options CheckerOptions `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RulePrecheckResponse is the body returned by POST /definition. The
|
||||||
|
// embedded *CheckerDefinition mirrors GET /definition so a client can
|
||||||
|
// fetch the full definition and precheck results in one round-trip.
|
||||||
|
// Keys in PrecheckFailures are rule names; values are the precheck
|
||||||
|
// error messages. Rules that do not implement RulePrecheck, or whose
|
||||||
|
// Precheck returned nil for the given options, are absent from the map.
|
||||||
|
type RulePrecheckResponse struct {
|
||||||
|
*CheckerDefinition
|
||||||
|
PrecheckFailures map[string]string `json:"precheck_failures"`
|
||||||
|
|
||||||
|
// Eligible reports whether this checker is meaningful for the submitted
|
||||||
|
// target, as decided by the provider's CheckEnabler (if implemented). It
|
||||||
|
// is nil when the checker does not implement CheckEnabler, or when
|
||||||
|
// IsEligible could not determine eligibility (its error was non-nil). A
|
||||||
|
// non-nil false means the checker is definitively not applicable to this
|
||||||
|
// target; the host should hide it unless Eligible != nil && !*Eligible.
|
||||||
|
Eligible *bool `json:"eligible,omitempty"`
|
||||||
|
|
||||||
|
// EligibilityReason explains a false Eligible, or carries the lookup error
|
||||||
|
// message when eligibility could not be determined. Empty otherwise.
|
||||||
|
EligibilityReason string `json:"eligibility_reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// ObservationGetter provides access to observation data (used by CheckRule).
|
// ObservationGetter provides access to observation data (used by CheckRule).
|
||||||
// Get unmarshals observation data into dest (like json.Unmarshal).
|
// Get unmarshals observation data into dest (like json.Unmarshal).
|
||||||
//
|
//
|
||||||
|
|
@ -421,10 +486,17 @@ type OptionsValidator interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExternalCollectRequest is sent to POST /collect on a remote checker endpoint.
|
// ExternalCollectRequest is sent to POST /collect on a remote checker endpoint.
|
||||||
|
//
|
||||||
|
// EnabledRules lets the host inform the provider which rules will be evaluated
|
||||||
|
// downstream, so the provider can skip optional work (network calls, paid API
|
||||||
|
// hits, …) for data that would not surface in any state. nil means "run
|
||||||
|
// everything"; an explicit map with a rule name set to false means that rule
|
||||||
|
// is off. Providers access the value via EnabledRulesFromContext(ctx).
|
||||||
type ExternalCollectRequest struct {
|
type ExternalCollectRequest struct {
|
||||||
Key ObservationKey `json:"key"`
|
Key ObservationKey `json:"key"`
|
||||||
Target CheckTarget `json:"target"`
|
Target CheckTarget `json:"target"`
|
||||||
Options CheckerOptions `json:"options"`
|
Options CheckerOptions `json:"options"`
|
||||||
|
EnabledRules map[string]bool `json:"enabledRules,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExternalCollectResponse is returned by POST /collect on a remote checker endpoint.
|
// ExternalCollectResponse is returned by POST /collect on a remote checker endpoint.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue