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:
parent
6070931025
commit
3016a8ee88
12 changed files with 1472 additions and 9 deletions
579
internal/storage/inmemory/checker.go
Normal file
579
internal/storage/inmemory/checker.go
Normal 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
|
||||
}
|
||||
|
|
@ -50,6 +50,13 @@ type InMemoryStorage struct {
|
|||
zones map[string]*happydns.ZoneMessage
|
||||
lastInsightsRun *time.Time
|
||||
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.
|
||||
|
|
@ -66,6 +73,11 @@ func NewInMemoryStorage() (*InMemoryStorage, error) {
|
|||
users: make(map[string]*happydns.User),
|
||||
usersByEmail: make(map[string]*happydns.User),
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ package storage // import "git.happydns.org/happyDomain/internal/storage"
|
|||
|
||||
import (
|
||||
"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_log"
|
||||
"git.happydns.org/happyDomain/internal/usecase/insight"
|
||||
|
|
@ -40,6 +41,12 @@ type ProviderAndDomainStorage interface {
|
|||
|
||||
type Storage interface {
|
||||
authuser.AuthUserStorage
|
||||
checker.CheckPlanStorage
|
||||
checker.CheckerOptionsStorage
|
||||
checker.CheckEvaluationStorage
|
||||
checker.ExecutionStorage
|
||||
checker.ObservationSnapshotStorage
|
||||
checker.SchedulerStateStorage
|
||||
domain.DomainStorage
|
||||
domainlog.DomainLogStorage
|
||||
insight.InsightStorage
|
||||
|
|
|
|||
189
internal/storage/kvtpl/check_evaluation.go
Normal file
189
internal/storage/kvtpl/check_evaluation.go
Normal 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
|
||||
}
|
||||
126
internal/storage/kvtpl/check_plan.go
Normal file
126
internal/storage/kvtpl/check_plan.go
Normal 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
|
||||
}
|
||||
119
internal/storage/kvtpl/checker_options.go
Normal file
119
internal/storage/kvtpl/checker_options.go
Normal 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
|
||||
}
|
||||
188
internal/storage/kvtpl/execution.go
Normal file
188
internal/storage/kvtpl/execution.go
Normal 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
|
||||
}
|
||||
63
internal/storage/kvtpl/observation_snapshot.go
Normal file
63
internal/storage/kvtpl/observation_snapshot.go
Normal 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
|
||||
}
|
||||
44
internal/storage/kvtpl/scheduler_state.go
Normal file
44
internal/storage/kvtpl/scheduler_state.go
Normal 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)
|
||||
}
|
||||
23
internal/usecase/checker/doc.go
Normal file
23
internal/usecase/checker/doc.go
Normal 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"
|
||||
108
internal/usecase/checker/storage.go
Normal file
108
internal/usecase/checker/storage.go
Normal 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
|
||||
}
|
||||
|
|
@ -27,15 +27,20 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrAuthUserNotFound = errors.New("user not found")
|
||||
ErrDomainNotFound = errors.New("domain not found")
|
||||
ErrDomainLogNotFound = errors.New("domain log not found")
|
||||
ErrProviderNotFound = errors.New("provider not found")
|
||||
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")
|
||||
ErrAuthUserNotFound = errors.New("user not found")
|
||||
ErrDomainNotFound = errors.New("domain not found")
|
||||
ErrDomainLogNotFound = errors.New("domain log not found")
|
||||
ErrProviderNotFound = errors.New("provider not found")
|
||||
ErrSessionNotFound = errors.New("session not found")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrUserAlreadyExist = errors.New("user already exists")
|
||||
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."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue