checkers: add domain model types and checker core registry
Introduce the foundational types for the checker system: - CheckTarget, CheckPlan, Execution, CheckEvaluation model types - CheckerDefinition, CheckerOptions, ObservationSnapshot types - CheckRule, ObservationProvider, CheckAggregator interfaces - CheckerEngine interface for orchestrating check pipelines - Global checker and observation provider registries - WorstStatusAggregator for combining rule results - New error types, config option for max concurrency, and AutoFill constants for context-driven option resolution Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
13a58d18d3
commit
eb347a4318
9 changed files with 651 additions and 2 deletions
48
internal/checker/aggregator.go
Normal file
48
internal/checker/aggregator.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package checker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// WorstStatusAggregator aggregates check states by taking the worst status.
|
||||
type WorstStatusAggregator struct{}
|
||||
|
||||
func (a WorstStatusAggregator) Aggregate(states []happydns.CheckState) happydns.CheckState {
|
||||
worst := happydns.StatusOK
|
||||
var messages []string
|
||||
for _, s := range states {
|
||||
if s.Status > worst {
|
||||
worst = s.Status
|
||||
}
|
||||
if s.Message != "" {
|
||||
messages = append(messages, s.Message)
|
||||
}
|
||||
}
|
||||
return happydns.CheckState{
|
||||
Status: worst,
|
||||
Message: strings.Join(messages, "; "),
|
||||
}
|
||||
}
|
||||
125
internal/checker/observation.go
Normal file
125
internal/checker/observation.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// providerRegistry is the global registry for observation providers.
|
||||
// Thread-safety: all writes happen during init() before any goroutines start.
|
||||
// After initialization, the map is read-only and safe for concurrent access.
|
||||
var providerRegistry = map[happydns.ObservationKey]happydns.ObservationProvider{}
|
||||
|
||||
// RegisterObservationProvider registers an observation provider globally.
|
||||
func RegisterObservationProvider(p happydns.ObservationProvider) {
|
||||
providerRegistry[p.Key()] = p
|
||||
}
|
||||
|
||||
// GetObservationProvider returns the provider for the given key, or nil.
|
||||
func GetObservationProvider(key happydns.ObservationKey) happydns.ObservationProvider {
|
||||
return providerRegistry[key]
|
||||
}
|
||||
|
||||
// GetObservationProviders returns all registered observation providers.
|
||||
func GetObservationProviders() map[happydns.ObservationKey]happydns.ObservationProvider {
|
||||
return providerRegistry
|
||||
}
|
||||
|
||||
// ObservationContext provides lazy-loading, cached, thread-safe access to observation data.
|
||||
type ObservationContext struct {
|
||||
target happydns.CheckTarget
|
||||
opts happydns.CheckerOptions
|
||||
cache map[happydns.ObservationKey]any
|
||||
errors map[happydns.ObservationKey]error
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewObservationContext creates a new ObservationContext for the given target and options.
|
||||
func NewObservationContext(target happydns.CheckTarget, opts happydns.CheckerOptions) *ObservationContext {
|
||||
return &ObservationContext{
|
||||
target: target,
|
||||
opts: opts,
|
||||
cache: make(map[happydns.ObservationKey]any),
|
||||
errors: make(map[happydns.ObservationKey]error),
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the observation data for the given key, collecting it lazily if needed.
|
||||
// Thread-safe: concurrent calls for the same key will only trigger one collection.
|
||||
func (oc *ObservationContext) Get(ctx context.Context, key happydns.ObservationKey) (any, error) {
|
||||
// Fast path: check cache under read lock.
|
||||
oc.mu.RLock()
|
||||
if val, ok := oc.cache[key]; ok {
|
||||
oc.mu.RUnlock()
|
||||
return val, nil
|
||||
}
|
||||
if err, ok := oc.errors[key]; ok {
|
||||
oc.mu.RUnlock()
|
||||
return nil, err
|
||||
}
|
||||
oc.mu.RUnlock()
|
||||
|
||||
// Slow path: acquire write lock and collect.
|
||||
oc.mu.Lock()
|
||||
defer oc.mu.Unlock()
|
||||
|
||||
// Double-check after acquiring write lock.
|
||||
if val, ok := oc.cache[key]; ok {
|
||||
return val, nil
|
||||
}
|
||||
if err, ok := oc.errors[key]; ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider := GetObservationProvider(key)
|
||||
if provider == nil {
|
||||
err := fmt.Errorf("no observation provider registered for key %q", key)
|
||||
oc.errors[key] = err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
val, err := provider.Collect(ctx, oc.target, oc.opts)
|
||||
if err != nil {
|
||||
oc.errors[key] = err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oc.cache[key] = val
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Data returns all cached observation data.
|
||||
func (oc *ObservationContext) Data() map[happydns.ObservationKey]any {
|
||||
oc.mu.RLock()
|
||||
defer oc.mu.RUnlock()
|
||||
|
||||
data := make(map[happydns.ObservationKey]any, len(oc.cache))
|
||||
for k, v := range oc.cache {
|
||||
data[k] = v
|
||||
}
|
||||
return data
|
||||
}
|
||||
50
internal/checker/registry.go
Normal file
50
internal/checker/registry.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package checker
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// checkerRegistry is the global registry for checker definitions.
|
||||
// Thread-safety: all writes happen during init() before any goroutines start.
|
||||
// After initialization, the map is read-only and safe for concurrent access.
|
||||
var checkerRegistry = map[string]*happydns.CheckerDefinition{}
|
||||
|
||||
// RegisterChecker registers a checker definition globally.
|
||||
func RegisterChecker(c *happydns.CheckerDefinition) {
|
||||
log.Println("Registering new checker:", c.ID)
|
||||
c.BuildRulesInfo()
|
||||
checkerRegistry[c.ID] = c
|
||||
}
|
||||
|
||||
// GetCheckers returns all registered checker definitions.
|
||||
func GetCheckers() map[string]*happydns.CheckerDefinition {
|
||||
return checkerRegistry
|
||||
}
|
||||
|
||||
// FindChecker returns the checker definition with the given ID, or nil.
|
||||
func FindChecker(id string) *happydns.CheckerDefinition {
|
||||
return checkerRegistry[id]
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ package config // import "git.happydns.org/happyDomain/config"
|
|||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/storage"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
|
|
@ -45,6 +46,7 @@ func declareFlags(o *happydns.Options) {
|
|||
flag.Var(&JWTSecretKey{&o.JWTSecretKey}, "jwt-secret-key", "Secret key used to verify JWT authentication tokens (a random secret is used if undefined)")
|
||||
flag.Var(&URL{&o.ExternalAuth}, "external-auth", "Base URL to use for login and registration (use embedded forms if left empty)")
|
||||
flag.BoolVar(&o.OptOutInsights, "opt-out-insights", false, "Disable the anonymous usage statistics report. If you care about this project and don't participate in discussions, don't opt-out.")
|
||||
flag.IntVar(&o.CheckerMaxConcurrency, "checker-max-concurrency", runtime.NumCPU(), "Maximum number of checker jobs that can run simultaneously")
|
||||
|
||||
flag.Var(&URL{&o.ListmonkURL}, "newsletter-server-url", "Base URL of the listmonk newsletter server")
|
||||
flag.IntVar(&o.ListmonkID, "newsletter-id", 1, "Listmonk identifier of the list receiving the new user")
|
||||
|
|
|
|||
403
model/checker.go
Normal file
403
model/checker.go
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package happydns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CheckScopeType represents the scope level of a check target.
|
||||
type CheckScopeType int
|
||||
|
||||
const (
|
||||
CheckScopeAdmin CheckScopeType = 0
|
||||
CheckScopeUser CheckScopeType = iota
|
||||
CheckScopeDomain
|
||||
CheckScopeZone
|
||||
CheckScopeService
|
||||
)
|
||||
|
||||
const (
|
||||
AutoFillDomainName = "domain_name"
|
||||
AutoFillSubdomain = "subdomain"
|
||||
AutoFillZone = "zone"
|
||||
AutoFillServiceType = "service_type"
|
||||
AutoFillService = "service"
|
||||
)
|
||||
|
||||
// CheckTarget identifies the resource a check applies to.
|
||||
type CheckTarget struct {
|
||||
UserId *Identifier `json:"userId,omitempty"`
|
||||
DomainId *Identifier `json:"domainId,omitempty"`
|
||||
ServiceId *Identifier `json:"serviceId,omitempty"`
|
||||
}
|
||||
|
||||
// Scope returns the most specific scope level of this target.
|
||||
func (t CheckTarget) Scope() CheckScopeType {
|
||||
if t.ServiceId != nil {
|
||||
return CheckScopeService
|
||||
}
|
||||
if t.DomainId != nil {
|
||||
return CheckScopeDomain
|
||||
}
|
||||
return CheckScopeUser
|
||||
}
|
||||
|
||||
// String returns a stable string representation of the target.
|
||||
func (t CheckTarget) String() string {
|
||||
var parts []string
|
||||
if t.UserId != nil {
|
||||
parts = append(parts, t.UserId.String())
|
||||
}
|
||||
if t.DomainId != nil {
|
||||
parts = append(parts, t.DomainId.String())
|
||||
}
|
||||
if t.ServiceId != nil {
|
||||
parts = append(parts, t.ServiceId.String())
|
||||
}
|
||||
return strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
// CheckerAvailability declares on which scopes a checker can operate.
|
||||
type CheckerAvailability struct {
|
||||
ApplyToDomain bool `json:"applyToDomain,omitempty"`
|
||||
ApplyToZone bool `json:"applyToZone,omitempty"`
|
||||
ApplyToService bool `json:"applyToService,omitempty"`
|
||||
LimitToProviders []string `json:"limitToProviders,omitempty"`
|
||||
LimitToServices []string `json:"limitToServices,omitempty"`
|
||||
}
|
||||
|
||||
// CheckerOptions holds the runtime options for a checker execution.
|
||||
type CheckerOptions map[string]any
|
||||
|
||||
// CheckerRunRequest is the JSON body for manually triggering a checker.
|
||||
type CheckerRunRequest struct {
|
||||
Options CheckerOptions `json:"options,omitempty"`
|
||||
EnabledRules map[string]bool `json:"enabledRules,omitempty"`
|
||||
}
|
||||
|
||||
// CheckerOptionDocumentation describes a single checker option.
|
||||
type CheckerOptionDocumentation = Field
|
||||
|
||||
// CheckerOptionsDocumentation describes all options a checker accepts, organized by level.
|
||||
type CheckerOptionsDocumentation struct {
|
||||
AdminOpts []CheckerOptionDocumentation `json:"adminOpts,omitempty"`
|
||||
UserOpts []CheckerOptionDocumentation `json:"userOpts,omitempty"`
|
||||
DomainOpts []CheckerOptionDocumentation `json:"domainOpts,omitempty"`
|
||||
ServiceOpts []CheckerOptionDocumentation `json:"serviceOpts,omitempty"`
|
||||
RunOpts []CheckerOptionDocumentation `json:"runOpts,omitempty"`
|
||||
}
|
||||
|
||||
// CheckerOptionsPositional stores options with their positional key components.
|
||||
type CheckerOptionsPositional struct {
|
||||
CheckName string `json:"checkName"`
|
||||
UserId *Identifier `json:"userId,omitempty"`
|
||||
DomainId *Identifier `json:"domainId,omitempty"`
|
||||
ServiceId *Identifier `json:"serviceId,omitempty"`
|
||||
|
||||
Options CheckerOptions `json:"options"`
|
||||
}
|
||||
|
||||
// Status represents the result status of a check evaluation.
|
||||
type Status int
|
||||
|
||||
const (
|
||||
StatusUnknown Status = iota
|
||||
StatusOK
|
||||
StatusInfo
|
||||
StatusWarn
|
||||
StatusCrit
|
||||
StatusError
|
||||
)
|
||||
|
||||
// String returns the human-readable name of the status.
|
||||
func (s Status) String() string {
|
||||
switch s {
|
||||
case StatusUnknown:
|
||||
return "UNKNOWN"
|
||||
case StatusOK:
|
||||
return "OK"
|
||||
case StatusInfo:
|
||||
return "INFO"
|
||||
case StatusWarn:
|
||||
return "WARN"
|
||||
case StatusCrit:
|
||||
return "CRIT"
|
||||
case StatusError:
|
||||
return "ERROR"
|
||||
default:
|
||||
return fmt.Sprintf("Status(%d)", int(s))
|
||||
}
|
||||
}
|
||||
|
||||
// CheckState is the result of evaluating a single rule.
|
||||
type CheckState struct {
|
||||
Status Status `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Meta map[string]any `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// CheckMetric represents a single metric produced by a check.
|
||||
type CheckMetric struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Value float64 `json:"value" binding:"required"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp" binding:"required" format:"date-time"`
|
||||
}
|
||||
|
||||
// ObservationKey identifies a type of observation data.
|
||||
type ObservationKey = string
|
||||
|
||||
// CheckIntervalSpec defines scheduling bounds for a checker.
|
||||
type CheckIntervalSpec struct {
|
||||
Min time.Duration `json:"min" swaggertype:"integer"`
|
||||
Max time.Duration `json:"max" swaggertype:"integer"`
|
||||
Default time.Duration `json:"default" swaggertype:"integer"`
|
||||
}
|
||||
|
||||
// CheckPlan is an optional user override for a checker on a specific target.
|
||||
type CheckPlan struct {
|
||||
Id Identifier `json:"id" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
CheckerID string `json:"checkerId" binding:"required" readonly:"true"`
|
||||
Target CheckTarget `json:"target" binding:"required" readonly:"true"`
|
||||
Interval *time.Duration `json:"interval,omitempty" swaggertype:"integer"`
|
||||
Enabled map[string]bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
||||
// IsFullyDisabled returns true if the enabled map is non-empty and every entry is false.
|
||||
func (p *CheckPlan) IsFullyDisabled() bool {
|
||||
if len(p.Enabled) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, v := range p.Enabled {
|
||||
if v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsRuleEnabled returns whether a specific rule is enabled.
|
||||
// A nil or empty map means all rules are enabled. A missing key means enabled.
|
||||
func (p *CheckPlan) IsRuleEnabled(ruleName string) bool {
|
||||
if len(p.Enabled) == 0 {
|
||||
return true
|
||||
}
|
||||
v, ok := p.Enabled[ruleName]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// CheckerStatus combines a checker definition with its latest execution and plan for a target.
|
||||
type CheckerStatus struct {
|
||||
*CheckerDefinition
|
||||
LatestExecution *Execution `json:"latestExecution,omitempty"`
|
||||
Plan *CheckPlan `json:"plan,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnabledRules map[string]bool `json:"enabledRules"`
|
||||
}
|
||||
|
||||
// CheckEvaluation is the result of running a checker on observed data.
|
||||
type CheckEvaluation struct {
|
||||
Id Identifier `json:"id" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
PlanID *Identifier `json:"planId,omitempty" swaggertype:"string"`
|
||||
CheckerID string `json:"checkerId" binding:"required"`
|
||||
Target CheckTarget `json:"target" binding:"required"`
|
||||
SnapshotID Identifier `json:"snapshotId" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
EvaluatedAt time.Time `json:"evaluatedAt" binding:"required" readonly:"true" format:"date-time"`
|
||||
States []CheckState `json:"states" binding:"required" readonly:"true"`
|
||||
}
|
||||
|
||||
// ObservationSnapshot holds data collected during an execution.
|
||||
type ObservationSnapshot struct {
|
||||
Id Identifier `json:"id" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
Target CheckTarget `json:"target" binding:"required" readonly:"true"`
|
||||
CollectedAt time.Time `json:"collectedAt" binding:"required" readonly:"true" format:"date-time"`
|
||||
Data map[ObservationKey]any `json:"data" binding:"required" readonly:"true"`
|
||||
}
|
||||
|
||||
// ExecutionStatus represents the lifecycle state of an execution.
|
||||
type ExecutionStatus int
|
||||
|
||||
const (
|
||||
ExecutionPending ExecutionStatus = iota
|
||||
ExecutionRunning
|
||||
ExecutionDone
|
||||
ExecutionFailed
|
||||
)
|
||||
|
||||
// TriggerType represents what initiated an execution.
|
||||
type TriggerType int
|
||||
|
||||
const (
|
||||
TriggerManual TriggerType = iota
|
||||
TriggerSchedule
|
||||
)
|
||||
|
||||
// TriggerInfo describes the trigger for an execution.
|
||||
type TriggerInfo struct {
|
||||
Type TriggerType `json:"type"`
|
||||
PlanID *Identifier `json:"planId,omitempty" swaggertype:"string"`
|
||||
}
|
||||
|
||||
// Execution represents a single run of a checker pipeline.
|
||||
type Execution struct {
|
||||
Id Identifier `json:"id" swaggertype:"string" binding:"required" readonly:"true"`
|
||||
CheckerID string `json:"checkerId" binding:"required" readonly:"true"`
|
||||
PlanID *Identifier `json:"planId,omitempty" swaggertype:"string" readonly:"true"`
|
||||
Target CheckTarget `json:"target" binding:"required" readonly:"true"`
|
||||
Trigger TriggerInfo `json:"trigger" binding:"required" readonly:"true"`
|
||||
StartedAt time.Time `json:"startedAt" binding:"required" readonly:"true" format:"date-time"`
|
||||
EndedAt *time.Time `json:"endedAt,omitempty" readonly:"true" format:"date-time"`
|
||||
Status ExecutionStatus `json:"status" binding:"required" readonly:"true"`
|
||||
Error string `json:"error,omitempty" readonly:"true"`
|
||||
Result CheckState `json:"result" readonly:"true"`
|
||||
EvaluationID *Identifier `json:"evaluationId,omitempty" swaggertype:"string" readonly:"true"`
|
||||
}
|
||||
|
||||
// ObservationProvider collects a specific type of data for a target.
|
||||
type ObservationProvider interface {
|
||||
Key() ObservationKey
|
||||
Collect(ctx context.Context, target CheckTarget, opts CheckerOptions) (any, error)
|
||||
}
|
||||
|
||||
// CheckRuleInfo is the JSON-serializable description of a rule, for API/UI listing.
|
||||
type CheckRuleInfo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Options *CheckerOptionsDocumentation `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// CheckRule evaluates observations and produces a CheckState.
|
||||
type CheckRule interface {
|
||||
Name() string
|
||||
Description() string
|
||||
Evaluate(ctx context.Context, obs ObservationGetter, opts CheckerOptions) CheckState
|
||||
}
|
||||
|
||||
// CheckRuleWithOptions is an optional interface that rules can implement
|
||||
// to declare their own options documentation for API/UI grouping.
|
||||
type CheckRuleWithOptions interface {
|
||||
CheckRule
|
||||
Options() CheckerOptionsDocumentation
|
||||
}
|
||||
|
||||
// ObservationGetter provides access to observation data (used by CheckRule).
|
||||
type ObservationGetter interface {
|
||||
Get(ctx context.Context, key ObservationKey) (any, error)
|
||||
}
|
||||
|
||||
// CheckAggregator combines multiple CheckStates into a single result.
|
||||
type CheckAggregator interface {
|
||||
Aggregate(states []CheckState) CheckState
|
||||
}
|
||||
|
||||
// CheckerDefinition is the complete definition of a checker, registered via init().
|
||||
type CheckerDefinition struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Availability CheckerAvailability `json:"availability"`
|
||||
Options CheckerOptionsDocumentation `json:"options"`
|
||||
RulesInfo []CheckRuleInfo `json:"rules"`
|
||||
Rules []CheckRule `json:"-"`
|
||||
Aggregator CheckAggregator `json:"-"`
|
||||
Interval *CheckIntervalSpec `json:"interval,omitempty"`
|
||||
}
|
||||
|
||||
// BuildRulesInfo populates RulesInfo from the Rules slice.
|
||||
func (d *CheckerDefinition) BuildRulesInfo() {
|
||||
d.RulesInfo = make([]CheckRuleInfo, len(d.Rules))
|
||||
for i, rule := range d.Rules {
|
||||
info := CheckRuleInfo{
|
||||
Name: rule.Name(),
|
||||
Description: rule.Description(),
|
||||
}
|
||||
if rwo, ok := rule.(CheckRuleWithOptions); ok {
|
||||
opts := rwo.Options()
|
||||
info.Options = &opts
|
||||
}
|
||||
d.RulesInfo[i] = info
|
||||
}
|
||||
}
|
||||
|
||||
// OptionsValidator is an optional interface that checkers (or their rules/providers)
|
||||
// can implement to perform domain-specific validation of checker options.
|
||||
type OptionsValidator interface {
|
||||
ValidateOptions(opts CheckerOptions) error
|
||||
}
|
||||
|
||||
// CheckerEngine orchestrates the full checker pipeline.
|
||||
type CheckerEngine interface {
|
||||
CreateExecution(checkerID string, target CheckTarget, plan *CheckPlan) (*Execution, error)
|
||||
RunExecution(ctx context.Context, exec *Execution, plan *CheckPlan, runOpts CheckerOptions) (*CheckEvaluation, error)
|
||||
}
|
||||
|
||||
// CheckerOptionsKey builds the positional KV key for checker options.
|
||||
// Format: chckrcfg-{checkerName}/{userId}/{domainId}/{serviceId}
|
||||
func CheckerOptionsKey(checkerName string, userId *Identifier, domainId *Identifier, serviceId *Identifier) string {
|
||||
uid := ""
|
||||
if userId != nil {
|
||||
uid = userId.String()
|
||||
}
|
||||
did := ""
|
||||
if domainId != nil {
|
||||
did = domainId.String()
|
||||
}
|
||||
sid := ""
|
||||
if serviceId != nil {
|
||||
sid = serviceId.String()
|
||||
}
|
||||
return fmt.Sprintf("chckrcfg-%s/%s/%s/%s", checkerName, uid, did, sid)
|
||||
}
|
||||
|
||||
// ParseCheckerOptionsKey extracts the positional components from a KV key.
|
||||
func ParseCheckerOptionsKey(key string) (checkerName string, userId *Identifier, domainId *Identifier, serviceId *Identifier) {
|
||||
trimmed := strings.TrimPrefix(key, "chckrcfg-")
|
||||
parts := strings.SplitN(trimmed, "/", 4)
|
||||
if len(parts) < 4 {
|
||||
return trimmed, nil, nil, nil
|
||||
}
|
||||
|
||||
checkerName = parts[0]
|
||||
if parts[1] != "" {
|
||||
if id, err := NewIdentifierFromString(parts[1]); err == nil {
|
||||
userId = &id
|
||||
}
|
||||
}
|
||||
if parts[2] != "" {
|
||||
if id, err := NewIdentifierFromString(parts[2]); err == nil {
|
||||
domainId = &id
|
||||
}
|
||||
}
|
||||
if parts[3] != "" {
|
||||
if id, err := NewIdentifierFromString(parts[3]); err == nil {
|
||||
serviceId = &id
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -93,6 +93,10 @@ type Options struct {
|
|||
|
||||
OIDCClients []OIDCSettings
|
||||
|
||||
// CheckerMaxConcurrency is the maximum number of checker jobs that can
|
||||
// run simultaneously. Defaults to runtime.NumCPU().
|
||||
CheckerMaxConcurrency int
|
||||
|
||||
// CaptchaProvider selects the captcha provider ("hcaptcha", "recaptchav2", "turnstile", or "").
|
||||
CaptchaProvider string
|
||||
|
||||
|
|
|
|||
|
|
@ -34,8 +34,13 @@ var (
|
|||
ErrSessionNotFound = errors.New("session not found")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrUserAlreadyExist = errors.New("user already exists")
|
||||
ErrZoneNotFound = errors.New("zone not found")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrZoneNotFound = errors.New("zone not found")
|
||||
ErrCheckerNotFound = errors.New("checker not found")
|
||||
ErrCheckPlanNotFound = errors.New("check plan not found")
|
||||
ErrCheckEvaluationNotFound = errors.New("check evaluation not found")
|
||||
ErrExecutionNotFound = errors.New("execution not found")
|
||||
ErrSnapshotNotFound = errors.New("snapshot not found")
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
const TryAgainErr = "Sorry, we are currently unable to sent email validation link. Please try again later."
|
||||
|
|
|
|||
|
|
@ -104,6 +104,10 @@ type Field struct {
|
|||
|
||||
// Description stores an helpfull sentence describing the field.
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
// AutoFill indicates that this field is automatically filled by the system
|
||||
// based on execution context (e.g. domain name, zone, service type).
|
||||
AutoFill string `json:"autoFill,omitempty"`
|
||||
}
|
||||
|
||||
type FormState struct {
|
||||
|
|
|
|||
|
|
@ -154,6 +154,14 @@ type ZoneServices struct {
|
|||
Services []*Service `json:"services"`
|
||||
}
|
||||
|
||||
// ZoneWithServicesCheckStatus wraps a Zone with the worst check status for each service.
|
||||
type ZoneWithServicesCheckStatus struct {
|
||||
*Zone
|
||||
// ServicesCheckStatus holds the worst check status for each service,
|
||||
// keyed by service identifier string. Nil/absent if no results exist yet.
|
||||
ServicesCheckStatus map[string]*Status `json:"services_check_status,omitempty"`
|
||||
}
|
||||
|
||||
type ZoneUsecase interface {
|
||||
AddRecord(*Zone, string, Record) error
|
||||
CreateZone(*Zone) error
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue