checkers: add storage interfaces and implementations

Add the persistence layer for the checker system:
- Storage interfaces (CheckPlanStorage, CheckerOptionsStorage,
  CheckEvaluationStorage, ExecutionStorage, ObservationSnapshotStorage,
  SchedulerStateStorage) in the usecase/checker package
- KV-based implementations for LevelDB/Oracle NoSQL backends
- In-memory implementation for testing
- Integrate checker storage into the main Storage interface
This commit is contained in:
nemunaire 2026-04-04 11:52:48 +07:00
commit 3016a8ee88
12 changed files with 1472 additions and 9 deletions

View file

@ -0,0 +1,579 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 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 inmemory
import (
"fmt"
"sort"
"strings"
"time"
"git.happydns.org/happyDomain/model"
)
// sliceIterator implements happydns.Iterator[T] over an in-memory slice.
type sliceIterator[T any] struct {
keys []string
items []*T
index int
deleteFn func(key string) error
}
func newSliceIterator[T any](keys []string, items []*T, deleteFn func(key string) error) *sliceIterator[T] {
return &sliceIterator[T]{keys: keys, items: items, index: -1, deleteFn: deleteFn}
}
func (it *sliceIterator[T]) Next() bool {
it.index++
return it.index < len(it.items)
}
func (it *sliceIterator[T]) NextWithError() bool { return it.Next() }
func (it *sliceIterator[T]) Item() *T {
if it.index < 0 || it.index >= len(it.items) {
return nil
}
return it.items[it.index]
}
func (it *sliceIterator[T]) DropItem() error {
if it.index < 0 || it.index >= len(it.keys) {
return fmt.Errorf("DropItem: iterator is not valid")
}
if it.deleteFn != nil {
return it.deleteFn(it.keys[it.index])
}
return nil
}
func (it *sliceIterator[T]) Key() string {
if it.index < 0 || it.index >= len(it.keys) {
return ""
}
return it.keys[it.index]
}
func (it *sliceIterator[T]) Raw() any { return it.Item() }
func (it *sliceIterator[T]) Err() error { return nil }
func (it *sliceIterator[T]) Close() {}
// --- CheckPlanStorage ---
func (s *InMemoryStorage) ListAllCheckPlans() (happydns.Iterator[happydns.CheckPlan], error) {
s.mu.Lock()
defer s.mu.Unlock()
keys := make([]string, 0, len(s.checkPlans))
items := make([]*happydns.CheckPlan, 0, len(s.checkPlans))
for k, p := range s.checkPlans {
keys = append(keys, k)
cp := *p
items = append(items, &cp)
}
return newSliceIterator(keys, items, func(key string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.checkPlans, key)
return nil
}), nil
}
func (s *InMemoryStorage) ListCheckPlansByTarget(target happydns.CheckTarget) ([]*happydns.CheckPlan, error) {
s.mu.Lock()
defer s.mu.Unlock()
var plans []*happydns.CheckPlan
for _, p := range s.checkPlans {
if p.Target.String() == target.String() {
cp := *p
plans = append(plans, &cp)
}
}
return plans, nil
}
func (s *InMemoryStorage) ListCheckPlansByChecker(checkerID string) ([]*happydns.CheckPlan, error) {
s.mu.Lock()
defer s.mu.Unlock()
var plans []*happydns.CheckPlan
for _, p := range s.checkPlans {
if p.CheckerID == checkerID {
cp := *p
plans = append(plans, &cp)
}
}
return plans, nil
}
func (s *InMemoryStorage) ListCheckPlansByUser(userId happydns.Identifier) ([]*happydns.CheckPlan, error) {
s.mu.Lock()
defer s.mu.Unlock()
var plans []*happydns.CheckPlan
for _, p := range s.checkPlans {
if p.Target.UserId == userId.String() {
cp := *p
plans = append(plans, &cp)
}
}
return plans, nil
}
func (s *InMemoryStorage) GetCheckPlan(planID happydns.Identifier) (*happydns.CheckPlan, error) {
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.checkPlans[planID.String()]
if !ok {
return nil, happydns.ErrCheckPlanNotFound
}
cp := *p
return &cp, nil
}
func (s *InMemoryStorage) CreateCheckPlan(plan *happydns.CheckPlan) error {
id, err := happydns.NewRandomIdentifier()
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
plan.Id = id
cp := *plan
s.checkPlans[id.String()] = &cp
return nil
}
func (s *InMemoryStorage) UpdateCheckPlan(plan *happydns.CheckPlan) error {
s.mu.Lock()
defer s.mu.Unlock()
cp := *plan
s.checkPlans[plan.Id.String()] = &cp
return nil
}
func (s *InMemoryStorage) DeleteCheckPlan(planID happydns.Identifier) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.checkPlans, planID.String())
return nil
}
func (s *InMemoryStorage) ClearCheckPlans() error {
s.mu.Lock()
defer s.mu.Unlock()
s.checkPlans = make(map[string]*happydns.CheckPlan)
return nil
}
// --- CheckerOptionsStorage ---
func (s *InMemoryStorage) ListAllCheckerConfigurations() (happydns.Iterator[happydns.CheckerOptions], error) {
s.mu.Lock()
defer s.mu.Unlock()
keys := make([]string, 0, len(s.checkerOptions))
items := make([]*happydns.CheckerOptions, 0, len(s.checkerOptions))
for k, opts := range s.checkerOptions {
keys = append(keys, k)
co := opts
items = append(items, &co)
}
return newSliceIterator(keys, items, func(key string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.checkerOptions, key)
return nil
}), nil
}
func (s *InMemoryStorage) ListCheckerConfiguration(checkerName string) ([]*happydns.CheckerOptionsPositional, error) {
prefix := fmt.Sprintf("chckrcfg-%s/", checkerName)
s.mu.Lock()
defer s.mu.Unlock()
var results []*happydns.CheckerOptionsPositional
for k, opts := range s.checkerOptions {
if !strings.HasPrefix(k, prefix) {
continue
}
cn, uid, did, sid := happydns.ParseCheckerOptionsKey(k)
co := opts
results = append(results, &happydns.CheckerOptionsPositional{
CheckName: cn,
UserId: uid,
DomainId: did,
ServiceId: sid,
Options: co,
})
}
return results, nil
}
func (s *InMemoryStorage) GetCheckerConfiguration(checkerName string, userId *happydns.Identifier, domainId *happydns.Identifier, serviceId *happydns.Identifier) ([]*happydns.CheckerOptionsPositional, error) {
s.mu.Lock()
defer s.mu.Unlock()
var results []*happydns.CheckerOptionsPositional
tryGet := func(cn string, uid, did, sid *happydns.Identifier) {
key := happydns.CheckerOptionsKey(cn, uid, did, sid)
if opts, ok := s.checkerOptions[key]; ok {
co := opts
results = append(results, &happydns.CheckerOptionsPositional{
CheckName: cn,
UserId: uid,
DomainId: did,
ServiceId: sid,
Options: co,
})
}
}
tryGet(checkerName, nil, nil, nil)
if userId != nil {
tryGet(checkerName, userId, nil, nil)
}
if userId != nil && domainId != nil {
tryGet(checkerName, userId, domainId, nil)
}
if userId != nil && domainId != nil && serviceId != nil {
tryGet(checkerName, userId, domainId, serviceId)
}
return results, nil
}
func (s *InMemoryStorage) UpdateCheckerConfiguration(checkerName string, userId *happydns.Identifier, domainId *happydns.Identifier, serviceId *happydns.Identifier, opts happydns.CheckerOptions) error {
key := happydns.CheckerOptionsKey(checkerName, userId, domainId, serviceId)
s.mu.Lock()
defer s.mu.Unlock()
s.checkerOptions[key] = opts
return nil
}
func (s *InMemoryStorage) DeleteCheckerConfiguration(checkerName string, userId *happydns.Identifier, domainId *happydns.Identifier, serviceId *happydns.Identifier) error {
key := happydns.CheckerOptionsKey(checkerName, userId, domainId, serviceId)
s.mu.Lock()
defer s.mu.Unlock()
delete(s.checkerOptions, key)
return nil
}
func (s *InMemoryStorage) ClearCheckerConfigurations() error {
s.mu.Lock()
defer s.mu.Unlock()
s.checkerOptions = make(map[string]happydns.CheckerOptions)
return nil
}
// --- CheckEvaluationStorage ---
func (s *InMemoryStorage) ListEvaluationsByPlan(planID happydns.Identifier) ([]*happydns.CheckEvaluation, error) {
s.mu.Lock()
defer s.mu.Unlock()
var evals []*happydns.CheckEvaluation
for _, e := range s.evaluations {
if e.PlanID != nil && e.PlanID.String() == planID.String() {
ce := *e
evals = append(evals, &ce)
}
}
return evals, nil
}
func (s *InMemoryStorage) ListEvaluationsByChecker(checkerID string, target happydns.CheckTarget, limit int) ([]*happydns.CheckEvaluation, error) {
s.mu.Lock()
defer s.mu.Unlock()
tStr := target.String()
var evals []*happydns.CheckEvaluation
for _, e := range s.evaluations {
if e.CheckerID == checkerID && e.Target.String() == tStr {
ce := *e
evals = append(evals, &ce)
}
}
sort.Slice(evals, func(i, j int) bool {
return evals[i].EvaluatedAt.After(evals[j].EvaluatedAt)
})
if limit > 0 && len(evals) > limit {
evals = evals[:limit]
}
return evals, nil
}
func (s *InMemoryStorage) GetEvaluation(evalID happydns.Identifier) (*happydns.CheckEvaluation, error) {
s.mu.Lock()
defer s.mu.Unlock()
e, ok := s.evaluations[evalID.String()]
if !ok {
return nil, happydns.ErrCheckEvaluationNotFound
}
ce := *e
return &ce, nil
}
func (s *InMemoryStorage) GetLatestEvaluation(planID happydns.Identifier) (*happydns.CheckEvaluation, error) {
evals, err := s.ListEvaluationsByPlan(planID)
if err != nil {
return nil, err
}
if len(evals) == 0 {
return nil, happydns.ErrCheckEvaluationNotFound
}
latest := evals[0]
for _, e := range evals[1:] {
if e.EvaluatedAt.After(latest.EvaluatedAt) {
latest = e
}
}
return latest, nil
}
func (s *InMemoryStorage) CreateEvaluation(eval *happydns.CheckEvaluation) error {
id, err := happydns.NewRandomIdentifier()
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
eval.Id = id
ce := *eval
s.evaluations[id.String()] = &ce
return nil
}
func (s *InMemoryStorage) DeleteEvaluation(evalID happydns.Identifier) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.evaluations, evalID.String())
return nil
}
func (s *InMemoryStorage) DeleteEvaluationsByChecker(checkerID string, target happydns.CheckTarget) error {
s.mu.Lock()
defer s.mu.Unlock()
tStr := target.String()
for k, e := range s.evaluations {
if e.CheckerID == checkerID && e.Target.String() == tStr {
delete(s.evaluations, k)
}
}
return nil
}
func (s *InMemoryStorage) ClearEvaluations() error {
s.mu.Lock()
defer s.mu.Unlock()
s.evaluations = make(map[string]*happydns.CheckEvaluation)
return nil
}
// --- ExecutionStorage ---
func (s *InMemoryStorage) ListExecutionsByPlan(planID happydns.Identifier) ([]*happydns.Execution, error) {
s.mu.Lock()
defer s.mu.Unlock()
var execs []*happydns.Execution
for _, e := range s.executions {
if e.PlanID != nil && e.PlanID.String() == planID.String() {
ce := *e
execs = append(execs, &ce)
}
}
return execs, nil
}
func (s *InMemoryStorage) ListExecutionsByChecker(checkerID string, target happydns.CheckTarget, limit int) ([]*happydns.Execution, error) {
s.mu.Lock()
defer s.mu.Unlock()
tStr := target.String()
var execs []*happydns.Execution
for _, e := range s.executions {
if e.CheckerID == checkerID && e.Target.String() == tStr {
ce := *e
execs = append(execs, &ce)
}
}
sort.Slice(execs, func(i, j int) bool {
return execs[i].StartedAt.After(execs[j].StartedAt)
})
if limit > 0 && len(execs) > limit {
execs = execs[:limit]
}
return execs, nil
}
func (s *InMemoryStorage) GetExecution(execID happydns.Identifier) (*happydns.Execution, error) {
s.mu.Lock()
defer s.mu.Unlock()
e, ok := s.executions[execID.String()]
if !ok {
return nil, happydns.ErrExecutionNotFound
}
ce := *e
return &ce, nil
}
func (s *InMemoryStorage) CreateExecution(exec *happydns.Execution) error {
id, err := happydns.NewRandomIdentifier()
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
exec.Id = id
ce := *exec
s.executions[id.String()] = &ce
return nil
}
func (s *InMemoryStorage) UpdateExecution(exec *happydns.Execution) error {
s.mu.Lock()
defer s.mu.Unlock()
ce := *exec
s.executions[exec.Id.String()] = &ce
return nil
}
func (s *InMemoryStorage) DeleteExecution(execID happydns.Identifier) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.executions, execID.String())
return nil
}
func (s *InMemoryStorage) DeleteExecutionsByChecker(checkerID string, target happydns.CheckTarget) error {
s.mu.Lock()
defer s.mu.Unlock()
tStr := target.String()
for k, e := range s.executions {
if e.CheckerID == checkerID && e.Target.String() == tStr {
delete(s.executions, k)
}
}
return nil
}
func (s *InMemoryStorage) ClearExecutions() error {
s.mu.Lock()
defer s.mu.Unlock()
s.executions = make(map[string]*happydns.Execution)
return nil
}
// --- ObservationSnapshotStorage ---
func (s *InMemoryStorage) GetSnapshot(snapID happydns.Identifier) (*happydns.ObservationSnapshot, error) {
s.mu.Lock()
defer s.mu.Unlock()
snap, ok := s.snapshots[snapID.String()]
if !ok {
return nil, happydns.ErrSnapshotNotFound
}
cs := *snap
return &cs, nil
}
func (s *InMemoryStorage) CreateSnapshot(snap *happydns.ObservationSnapshot) error {
id, err := happydns.NewRandomIdentifier()
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
snap.Id = id
cs := *snap
s.snapshots[id.String()] = &cs
return nil
}
func (s *InMemoryStorage) DeleteSnapshot(snapID happydns.Identifier) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.snapshots, snapID.String())
return nil
}
func (s *InMemoryStorage) ClearSnapshots() error {
s.mu.Lock()
defer s.mu.Unlock()
s.snapshots = make(map[string]*happydns.ObservationSnapshot)
return nil
}
// --- SchedulerStateStorage ---
func (s *InMemoryStorage) GetLastSchedulerRun() (time.Time, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.schedulerLastRun == nil {
return time.Time{}, nil
}
return *s.schedulerLastRun, nil
}
func (s *InMemoryStorage) SetLastSchedulerRun(t time.Time) error {
s.mu.Lock()
defer s.mu.Unlock()
s.schedulerLastRun = &t
return nil
}

View file

@ -50,6 +50,13 @@ type InMemoryStorage struct {
zones map[string]*happydns.ZoneMessage zones map[string]*happydns.ZoneMessage
lastInsightsRun *time.Time lastInsightsRun *time.Time
lastInsightsID happydns.Identifier lastInsightsID happydns.Identifier
// Checker-related storage
checkPlans map[string]*happydns.CheckPlan
checkerOptions map[string]happydns.CheckerOptions
evaluations map[string]*happydns.CheckEvaluation
executions map[string]*happydns.Execution
snapshots map[string]*happydns.ObservationSnapshot
schedulerLastRun *time.Time
} }
// NewInMemoryStorage creates a new instance of InMemoryStorage. // NewInMemoryStorage creates a new instance of InMemoryStorage.
@ -66,6 +73,11 @@ func NewInMemoryStorage() (*InMemoryStorage, error) {
users: make(map[string]*happydns.User), users: make(map[string]*happydns.User),
usersByEmail: make(map[string]*happydns.User), usersByEmail: make(map[string]*happydns.User),
zones: make(map[string]*happydns.ZoneMessage), zones: make(map[string]*happydns.ZoneMessage),
checkPlans: make(map[string]*happydns.CheckPlan),
checkerOptions: make(map[string]happydns.CheckerOptions),
evaluations: make(map[string]*happydns.CheckEvaluation),
executions: make(map[string]*happydns.Execution),
snapshots: make(map[string]*happydns.ObservationSnapshot),
}, nil }, nil
} }

View file

@ -23,6 +23,7 @@ package storage // import "git.happydns.org/happyDomain/internal/storage"
import ( import (
"git.happydns.org/happyDomain/internal/usecase/authuser" "git.happydns.org/happyDomain/internal/usecase/authuser"
"git.happydns.org/happyDomain/internal/usecase/checker"
"git.happydns.org/happyDomain/internal/usecase/domain" "git.happydns.org/happyDomain/internal/usecase/domain"
"git.happydns.org/happyDomain/internal/usecase/domain_log" "git.happydns.org/happyDomain/internal/usecase/domain_log"
"git.happydns.org/happyDomain/internal/usecase/insight" "git.happydns.org/happyDomain/internal/usecase/insight"
@ -40,6 +41,12 @@ type ProviderAndDomainStorage interface {
type Storage interface { type Storage interface {
authuser.AuthUserStorage authuser.AuthUserStorage
checker.CheckPlanStorage
checker.CheckerOptionsStorage
checker.CheckEvaluationStorage
checker.ExecutionStorage
checker.ObservationSnapshotStorage
checker.SchedulerStateStorage
domain.DomainStorage domain.DomainStorage
domainlog.DomainLogStorage domainlog.DomainLogStorage
insight.InsightStorage insight.InsightStorage

View file

@ -0,0 +1,189 @@
// 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 database
import (
"errors"
"fmt"
"sort"
"git.happydns.org/happyDomain/model"
)
func (s *KVStorage) ListEvaluationsByPlan(planID happydns.Identifier) ([]*happydns.CheckEvaluation, error) {
prefix := fmt.Sprintf("chckeval-plan|%s|", planID.String())
iter := s.db.Search(prefix)
defer iter.Release()
var evals []*happydns.CheckEvaluation
for iter.Next() {
var eval happydns.CheckEvaluation
if err := s.db.DecodeData(iter.Value(), &eval); err != nil {
continue
}
evals = append(evals, &eval)
}
return evals, nil
}
func (s *KVStorage) GetEvaluation(evalID happydns.Identifier) (*happydns.CheckEvaluation, error) {
eval := &happydns.CheckEvaluation{}
err := s.db.Get(fmt.Sprintf("chckeval-%s", evalID.String()), eval)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrCheckEvaluationNotFound
}
return eval, err
}
func (s *KVStorage) GetLatestEvaluation(planID happydns.Identifier) (*happydns.CheckEvaluation, error) {
evals, err := s.ListEvaluationsByPlan(planID)
if err != nil {
return nil, err
}
if len(evals) == 0 {
return nil, happydns.ErrCheckEvaluationNotFound
}
latest := evals[0]
for _, e := range evals[1:] {
if e.EvaluatedAt.After(latest.EvaluatedAt) {
latest = e
}
}
return latest, nil
}
func (s *KVStorage) ListEvaluationsByChecker(checkerID string, target happydns.CheckTarget, limit int) ([]*happydns.CheckEvaluation, error) {
prefix := fmt.Sprintf("chckeval-chkr|%s|%s|", checkerID, target.String())
iter := s.db.Search(prefix)
defer iter.Release()
var evals []*happydns.CheckEvaluation
for iter.Next() {
var eval happydns.CheckEvaluation
if err := s.db.DecodeData(iter.Value(), &eval); err != nil {
continue
}
evals = append(evals, &eval)
}
// Sort by EvaluatedAt descending (most recent first).
sort.Slice(evals, func(i, j int) bool {
return evals[i].EvaluatedAt.After(evals[j].EvaluatedAt)
})
if limit > 0 && len(evals) > limit {
evals = evals[:limit]
}
return evals, nil
}
func (s *KVStorage) CreateEvaluation(eval *happydns.CheckEvaluation) error {
key, id, err := s.db.FindIdentifierKey("chckeval-")
if err != nil {
return err
}
eval.Id = id
// Store the primary record.
if err := s.db.Put(key, eval); err != nil {
return err
}
// Store secondary index by plan if applicable.
if eval.PlanID != nil {
indexKey := fmt.Sprintf("chckeval-plan|%s|%s", eval.PlanID.String(), eval.Id.String())
if err := s.db.Put(indexKey, eval); err != nil {
return err
}
}
// Store secondary index by checker+target.
checkerIndexKey := fmt.Sprintf("chckeval-chkr|%s|%s|%s", eval.CheckerID, eval.Target.String(), eval.Id.String())
if err := s.db.Put(checkerIndexKey, eval); err != nil {
return err
}
return nil
}
func (s *KVStorage) DeleteEvaluation(evalID happydns.Identifier) error {
// Load first to find plan ID for index cleanup.
eval, err := s.GetEvaluation(evalID)
if err != nil {
return err
}
if eval.PlanID != nil {
indexKey := fmt.Sprintf("chckeval-plan|%s|%s", eval.PlanID.String(), eval.Id.String())
_ = s.db.Delete(indexKey)
}
// Clean up checker+target index.
checkerIndexKey := fmt.Sprintf("chckeval-chkr|%s|%s|%s", eval.CheckerID, eval.Target.String(), eval.Id.String())
_ = s.db.Delete(checkerIndexKey)
return s.db.Delete(fmt.Sprintf("chckeval-%s", evalID.String()))
}
func (s *KVStorage) DeleteEvaluationsByChecker(checkerID string, target happydns.CheckTarget) error {
prefix := fmt.Sprintf("chckeval-chkr|%s|%s|", checkerID, target.String())
iter := s.db.Search(prefix)
defer iter.Release()
for iter.Next() {
var eval happydns.CheckEvaluation
if err := s.db.DecodeData(iter.Value(), &eval); err != nil {
continue
}
// Delete primary record.
_ = s.db.Delete(fmt.Sprintf("chckeval-%s", eval.Id.String()))
// Delete plan index if applicable.
if eval.PlanID != nil {
planIndexKey := fmt.Sprintf("chckeval-plan|%s|%s", eval.PlanID.String(), eval.Id.String())
_ = s.db.Delete(planIndexKey)
}
// Delete this checker index entry.
if err := s.db.Delete(iter.Key()); err != nil {
return err
}
}
return nil
}
func (s *KVStorage) ClearEvaluations() error {
// A single prefix scan covers primary records and all secondary indexes
// (chckeval-plan|... and chckeval-chkr|...) in one pass, avoiding
// double-delete errors that occurred when the indexes were deleted first
// and then matched again by the broader "chckeval-" prefix.
iter := s.db.Search("chckeval-")
defer iter.Release()
for iter.Next() {
if err := s.db.Delete(iter.Key()); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,126 @@
// 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 database
import (
"errors"
"fmt"
"git.happydns.org/happyDomain/model"
)
func (s *KVStorage) ListAllCheckPlans() (happydns.Iterator[happydns.CheckPlan], error) {
iter := s.db.Search("chckpln-")
return NewKVIterator[happydns.CheckPlan](s.db, iter), nil
}
func (s *KVStorage) ListCheckPlansByTarget(target happydns.CheckTarget) ([]*happydns.CheckPlan, error) {
iter, err := s.ListAllCheckPlans()
if err != nil {
return nil, err
}
defer iter.Close()
var plans []*happydns.CheckPlan
for iter.Next() {
plan := iter.Item()
if plan.Target.String() == target.String() {
plans = append(plans, plan)
}
}
return plans, nil
}
func (s *KVStorage) ListCheckPlansByChecker(checkerID string) ([]*happydns.CheckPlan, error) {
iter, err := s.ListAllCheckPlans()
if err != nil {
return nil, err
}
defer iter.Close()
var plans []*happydns.CheckPlan
for iter.Next() {
plan := iter.Item()
if plan.CheckerID == checkerID {
plans = append(plans, plan)
}
}
return plans, nil
}
func (s *KVStorage) ListCheckPlansByUser(userId happydns.Identifier) ([]*happydns.CheckPlan, error) {
iter, err := s.ListAllCheckPlans()
if err != nil {
return nil, err
}
defer iter.Close()
var plans []*happydns.CheckPlan
for iter.Next() {
plan := iter.Item()
if plan.Target.UserId == userId.String() {
plans = append(plans, plan)
}
}
return plans, nil
}
func (s *KVStorage) GetCheckPlan(planID happydns.Identifier) (*happydns.CheckPlan, error) {
plan := &happydns.CheckPlan{}
err := s.db.Get(fmt.Sprintf("chckpln-%s", planID.String()), plan)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrCheckPlanNotFound
}
return plan, err
}
func (s *KVStorage) CreateCheckPlan(plan *happydns.CheckPlan) error {
key, id, err := s.db.FindIdentifierKey("chckpln-")
if err != nil {
return err
}
plan.Id = id
return s.db.Put(key, plan)
}
func (s *KVStorage) UpdateCheckPlan(plan *happydns.CheckPlan) error {
return s.db.Put(fmt.Sprintf("chckpln-%s", plan.Id.String()), plan)
}
func (s *KVStorage) DeleteCheckPlan(planID happydns.Identifier) error {
return s.db.Delete(fmt.Sprintf("chckpln-%s", planID.String()))
}
func (s *KVStorage) ClearCheckPlans() error {
iter, err := s.ListAllCheckPlans()
if err != nil {
return err
}
defer iter.Close()
for iter.Next() {
if err := s.db.Delete(iter.Key()); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,119 @@
// 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 database
import (
"fmt"
"git.happydns.org/happyDomain/model"
)
func (s *KVStorage) ListAllCheckerConfigurations() (happydns.Iterator[happydns.CheckerOptions], error) {
iter := s.db.Search("chckrcfg-")
return NewKVIterator[happydns.CheckerOptions](s.db, iter), nil
}
func (s *KVStorage) ListCheckerConfiguration(checkerName string) ([]*happydns.CheckerOptionsPositional, error) {
prefix := fmt.Sprintf("chckrcfg-%s/", checkerName)
iter := s.db.Search(prefix)
defer iter.Release()
var results []*happydns.CheckerOptionsPositional
for iter.Next() {
var opts happydns.CheckerOptions
if err := s.db.DecodeData(iter.Value(), &opts); err != nil {
continue
}
cn, uid, did, sid := happydns.ParseCheckerOptionsKey(iter.Key())
results = append(results, &happydns.CheckerOptionsPositional{
CheckName: cn,
UserId: uid,
DomainId: did,
ServiceId: sid,
Options: opts,
})
}
return results, nil
}
func (s *KVStorage) GetCheckerConfiguration(checkerName string, userId *happydns.Identifier, domainId *happydns.Identifier, serviceId *happydns.Identifier) ([]*happydns.CheckerOptionsPositional, error) {
var results []*happydns.CheckerOptionsPositional
// Try each scope level from admin up to the requested specificity.
scopes := []struct {
uid, did, sid *happydns.Identifier
}{
{nil, nil, nil},
{userId, nil, nil},
{userId, domainId, nil},
{userId, domainId, serviceId},
}
for _, sc := range scopes {
// Skip levels that require identifiers not provided.
if (sc.uid != nil && userId == nil) || (sc.did != nil && domainId == nil) || (sc.sid != nil && serviceId == nil) {
continue
}
key := happydns.CheckerOptionsKey(checkerName, sc.uid, sc.did, sc.sid)
var opts happydns.CheckerOptions
if err := s.db.Get(key, &opts); err == nil {
results = append(results, &happydns.CheckerOptionsPositional{
CheckName: checkerName,
UserId: sc.uid,
DomainId: sc.did,
ServiceId: sc.sid,
Options: opts,
})
}
}
return results, nil
}
func (s *KVStorage) UpdateCheckerConfiguration(checkerName string, userId *happydns.Identifier, domainId *happydns.Identifier, serviceId *happydns.Identifier, opts happydns.CheckerOptions) error {
key := happydns.CheckerOptionsKey(checkerName, userId, domainId, serviceId)
return s.db.Put(key, opts)
}
func (s *KVStorage) DeleteCheckerConfiguration(checkerName string, userId *happydns.Identifier, domainId *happydns.Identifier, serviceId *happydns.Identifier) error {
key := happydns.CheckerOptionsKey(checkerName, userId, domainId, serviceId)
return s.db.Delete(key)
}
func (s *KVStorage) ClearCheckerConfigurations() error {
iter, err := s.ListAllCheckerConfigurations()
if err != nil {
return err
}
defer iter.Close()
for iter.Next() {
if err := s.db.Delete(iter.Key()); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,188 @@
// 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 database
import (
"errors"
"fmt"
"sort"
"git.happydns.org/happyDomain/model"
)
func (s *KVStorage) ListExecutionsByPlan(planID happydns.Identifier) ([]*happydns.Execution, error) {
prefix := fmt.Sprintf("chckexec-plan|%s|", planID.String())
iter := s.db.Search(prefix)
defer iter.Release()
var execs []*happydns.Execution
for iter.Next() {
var exec happydns.Execution
if err := s.db.DecodeData(iter.Value(), &exec); err != nil {
continue
}
execs = append(execs, &exec)
}
return execs, nil
}
func (s *KVStorage) ListExecutionsByChecker(checkerID string, target happydns.CheckTarget, limit int) ([]*happydns.Execution, error) {
prefix := fmt.Sprintf("chckexec-chkr|%s|%s|", checkerID, target.String())
iter := s.db.Search(prefix)
defer iter.Release()
var execs []*happydns.Execution
for iter.Next() {
var exec happydns.Execution
if err := s.db.DecodeData(iter.Value(), &exec); err != nil {
continue
}
execs = append(execs, &exec)
}
sort.Slice(execs, func(i, j int) bool {
return execs[i].StartedAt.After(execs[j].StartedAt)
})
if limit > 0 && len(execs) > limit {
execs = execs[:limit]
}
return execs, nil
}
func (s *KVStorage) GetExecution(execID happydns.Identifier) (*happydns.Execution, error) {
exec := &happydns.Execution{}
err := s.db.Get(fmt.Sprintf("chckexec-%s", execID.String()), exec)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrExecutionNotFound
}
return exec, err
}
func (s *KVStorage) CreateExecution(exec *happydns.Execution) error {
key, id, err := s.db.FindIdentifierKey("chckexec-")
if err != nil {
return err
}
exec.Id = id
if err := s.db.Put(key, exec); err != nil {
return err
}
// Secondary index by plan.
if exec.PlanID != nil {
indexKey := fmt.Sprintf("chckexec-plan|%s|%s", exec.PlanID.String(), exec.Id.String())
if err := s.db.Put(indexKey, exec); err != nil {
return err
}
}
// Secondary index by checker+target.
checkerIndexKey := fmt.Sprintf("chckexec-chkr|%s|%s|%s", exec.CheckerID, exec.Target.String(), exec.Id.String())
if err := s.db.Put(checkerIndexKey, exec); err != nil {
return err
}
return nil
}
func (s *KVStorage) UpdateExecution(exec *happydns.Execution) error {
if err := s.db.Put(fmt.Sprintf("chckexec-%s", exec.Id.String()), exec); err != nil {
return err
}
// Update secondary index by plan if applicable.
if exec.PlanID != nil {
indexKey := fmt.Sprintf("chckexec-plan|%s|%s", exec.PlanID.String(), exec.Id.String())
if err := s.db.Put(indexKey, exec); err != nil {
return err
}
}
// Update secondary index by checker+target.
checkerIndexKey := fmt.Sprintf("chckexec-chkr|%s|%s|%s", exec.CheckerID, exec.Target.String(), exec.Id.String())
if err := s.db.Put(checkerIndexKey, exec); err != nil {
return err
}
return nil
}
func (s *KVStorage) DeleteExecution(execID happydns.Identifier) error {
exec, err := s.GetExecution(execID)
if err != nil {
return err
}
if exec.PlanID != nil {
indexKey := fmt.Sprintf("chckexec-plan|%s|%s", exec.PlanID.String(), execID.String())
if err := s.db.Delete(indexKey); err != nil {
return err
}
}
checkerIndexKey := fmt.Sprintf("chckexec-chkr|%s|%s|%s", exec.CheckerID, exec.Target.String(), execID.String())
if err := s.db.Delete(checkerIndexKey); err != nil {
return err
}
return s.db.Delete(fmt.Sprintf("chckexec-%s", execID.String()))
}
func (s *KVStorage) DeleteExecutionsByChecker(checkerID string, target happydns.CheckTarget) error {
prefix := fmt.Sprintf("chckexec-chkr|%s|%s|", checkerID, target.String())
iter := s.db.Search(prefix)
defer iter.Release()
for iter.Next() {
var exec happydns.Execution
if err := s.db.DecodeData(iter.Value(), &exec); err != nil {
continue
}
_ = s.db.Delete(fmt.Sprintf("chckexec-%s", exec.Id.String()))
if exec.PlanID != nil {
planIndexKey := fmt.Sprintf("chckexec-plan|%s|%s", exec.PlanID.String(), exec.Id.String())
_ = s.db.Delete(planIndexKey)
}
if err := s.db.Delete(iter.Key()); err != nil {
return err
}
}
return nil
}
func (s *KVStorage) ClearExecutions() error {
// A single prefix scan covers primary records and all secondary indexes
// (chckexec-plan|... and chckexec-chkr|...) in one pass.
iter := s.db.Search("chckexec-")
defer iter.Release()
for iter.Next() {
if err := s.db.Delete(iter.Key()); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,63 @@
// 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 database
import (
"errors"
"fmt"
"git.happydns.org/happyDomain/model"
)
func (s *KVStorage) GetSnapshot(snapID happydns.Identifier) (*happydns.ObservationSnapshot, error) {
snap := &happydns.ObservationSnapshot{}
err := s.db.Get(fmt.Sprintf("chcksnap-%s", snapID.String()), snap)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrSnapshotNotFound
}
return snap, err
}
func (s *KVStorage) CreateSnapshot(snap *happydns.ObservationSnapshot) error {
key, id, err := s.db.FindIdentifierKey("chcksnap-")
if err != nil {
return err
}
snap.Id = id
return s.db.Put(key, snap)
}
func (s *KVStorage) DeleteSnapshot(snapID happydns.Identifier) error {
return s.db.Delete(fmt.Sprintf("chcksnap-%s", snapID.String()))
}
func (s *KVStorage) ClearSnapshots() error {
iter := s.db.Search("chcksnap-")
defer iter.Release()
for iter.Next() {
if err := s.db.Delete(iter.Key()); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,44 @@
// 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 database
import (
"errors"
"time"
"git.happydns.org/happyDomain/model"
)
const schedulerLastRunKey = "scheduler-lastrun"
func (s *KVStorage) GetLastSchedulerRun() (time.Time, error) {
var t time.Time
err := s.db.Get(schedulerLastRunKey, &t)
if errors.Is(err, happydns.ErrNotFound) {
return time.Time{}, nil
}
return t, err
}
func (s *KVStorage) SetLastSchedulerRun(t time.Time) error {
return s.db.Put(schedulerLastRunKey, t)
}

View file

@ -0,0 +1,23 @@
// 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 provides the usecase layer for the checker/monitoring system.
package checker // import "git.happydns.org/happyDomain/internal/usecase/checker"

View file

@ -0,0 +1,108 @@
// 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 (
"time"
"git.happydns.org/happyDomain/model"
)
// SchedulerStateStorage provides persistence for scheduler state (e.g. last run time).
type SchedulerStateStorage interface {
GetLastSchedulerRun() (time.Time, error)
SetLastSchedulerRun(t time.Time) error
}
// DomainLister is the minimal interface needed by the scheduler to enumerate domains.
type DomainLister interface {
ListAllDomains() (happydns.Iterator[happydns.Domain], error)
}
// CheckAutoFillStorage provides access to domain, zone and user data
// needed to resolve auto-fill field values at execution time.
type CheckAutoFillStorage interface {
GetDomain(id happydns.Identifier) (*happydns.Domain, error)
GetZone(id happydns.Identifier) (*happydns.ZoneMessage, error)
ListDomains(u *happydns.User) ([]*happydns.Domain, error)
GetUser(id happydns.Identifier) (*happydns.User, error)
}
// CheckPlanStorage provides persistence for CheckPlan entities.
type CheckPlanStorage interface {
ListAllCheckPlans() (happydns.Iterator[happydns.CheckPlan], error)
ListCheckPlansByTarget(target happydns.CheckTarget) ([]*happydns.CheckPlan, error)
ListCheckPlansByChecker(checkerID string) ([]*happydns.CheckPlan, error)
ListCheckPlansByUser(userId happydns.Identifier) ([]*happydns.CheckPlan, error)
GetCheckPlan(planID happydns.Identifier) (*happydns.CheckPlan, error)
CreateCheckPlan(plan *happydns.CheckPlan) error
UpdateCheckPlan(plan *happydns.CheckPlan) error
DeleteCheckPlan(planID happydns.Identifier) error
ClearCheckPlans() error
}
// CheckerOptionsStorage provides persistence for checker options at different levels.
type CheckerOptionsStorage interface {
ListAllCheckerConfigurations() (happydns.Iterator[happydns.CheckerOptions], error)
ListCheckerConfiguration(checkerName string) ([]*happydns.CheckerOptionsPositional, error)
GetCheckerConfiguration(checkerName string, userId *happydns.Identifier, domainId *happydns.Identifier, serviceId *happydns.Identifier) ([]*happydns.CheckerOptionsPositional, error)
UpdateCheckerConfiguration(checkerName string, userId *happydns.Identifier, domainId *happydns.Identifier, serviceId *happydns.Identifier, opts happydns.CheckerOptions) error
DeleteCheckerConfiguration(checkerName string, userId *happydns.Identifier, domainId *happydns.Identifier, serviceId *happydns.Identifier) error
ClearCheckerConfigurations() error
}
// CheckEvaluationStorage provides persistence for check evaluation results.
type CheckEvaluationStorage interface {
ListEvaluationsByPlan(planID happydns.Identifier) ([]*happydns.CheckEvaluation, error)
ListEvaluationsByChecker(checkerID string, target happydns.CheckTarget, limit int) ([]*happydns.CheckEvaluation, error)
GetEvaluation(evalID happydns.Identifier) (*happydns.CheckEvaluation, error)
GetLatestEvaluation(planID happydns.Identifier) (*happydns.CheckEvaluation, error)
CreateEvaluation(eval *happydns.CheckEvaluation) error
DeleteEvaluation(evalID happydns.Identifier) error
DeleteEvaluationsByChecker(checkerID string, target happydns.CheckTarget) error
ClearEvaluations() error
}
// ExecutionStorage provides persistence for execution records.
type ExecutionStorage interface {
ListExecutionsByPlan(planID happydns.Identifier) ([]*happydns.Execution, error)
ListExecutionsByChecker(checkerID string, target happydns.CheckTarget, limit int) ([]*happydns.Execution, error)
GetExecution(execID happydns.Identifier) (*happydns.Execution, error)
CreateExecution(exec *happydns.Execution) error
UpdateExecution(exec *happydns.Execution) error
DeleteExecution(execID happydns.Identifier) error
DeleteExecutionsByChecker(checkerID string, target happydns.CheckTarget) error
ClearExecutions() error
}
// PlannedJobProvider exposes upcoming scheduler jobs from the in-memory queue.
type PlannedJobProvider interface {
GetPlannedJobsForChecker(checkerID string, target happydns.CheckTarget) []*SchedulerJob
}
// ObservationSnapshotStorage provides persistence for observation snapshots.
type ObservationSnapshotStorage interface {
GetSnapshot(snapID happydns.Identifier) (*happydns.ObservationSnapshot, error)
CreateSnapshot(snap *happydns.ObservationSnapshot) error
DeleteSnapshot(snapID happydns.Identifier) error
ClearSnapshots() error
}

View file

@ -27,15 +27,20 @@ import (
) )
var ( var (
ErrAuthUserNotFound = errors.New("user not found") ErrAuthUserNotFound = errors.New("user not found")
ErrDomainNotFound = errors.New("domain not found") ErrDomainNotFound = errors.New("domain not found")
ErrDomainLogNotFound = errors.New("domain log not found") ErrDomainLogNotFound = errors.New("domain log not found")
ErrProviderNotFound = errors.New("provider not found") ErrProviderNotFound = errors.New("provider not found")
ErrSessionNotFound = errors.New("session not found") ErrSessionNotFound = errors.New("session not found")
ErrUserNotFound = errors.New("user not found") ErrUserNotFound = errors.New("user not found")
ErrUserAlreadyExist = errors.New("user already exists") ErrUserAlreadyExist = errors.New("user already exists")
ErrZoneNotFound = errors.New("zone not found") ErrZoneNotFound = errors.New("zone not found")
ErrNotFound = errors.New("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." const TryAgainErr = "Sorry, we are currently unable to sent email validation link. Please try again later."