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/InMemory backends - Integrate checker storage into the main Storage interface - Add tidy methods for checker entities (plans, configurations, evaluations, executions, snapshots, observation cache) and secondary index cleanup
316 lines
8.9 KiB
Go
316 lines
8.9 KiB
Go
// 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"
|
|
"log"
|
|
"sort"
|
|
"strings"
|
|
|
|
"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() {
|
|
evalId, err := lastKeySegment(iter.Key())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
eval, err := s.GetEvaluation(evalId)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
evals = append(evals, eval)
|
|
}
|
|
return evals, nil
|
|
}
|
|
|
|
func (s *KVStorage) ListAllEvaluations() (happydns.Iterator[happydns.CheckEvaluation], error) {
|
|
iter := s.db.Search("chckeval|")
|
|
return NewKVIterator[happydns.CheckEvaluation](s.db, iter), 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() {
|
|
evalId, err := lastKeySegment(iter.Key())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
eval, err := s.GetEvaluation(evalId)
|
|
if 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, true); 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, true); 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())
|
|
if err := s.db.Delete(indexKey); err != nil {
|
|
log.Printf("DeleteEvaluation: failed to delete plan index %s: %v\n", indexKey, err)
|
|
}
|
|
}
|
|
|
|
// Clean up checker+target index.
|
|
checkerIndexKey := fmt.Sprintf("chckeval-chkr|%s|%s|%s", eval.CheckerID, eval.Target.String(), eval.Id.String())
|
|
if err := s.db.Delete(checkerIndexKey); err != nil {
|
|
log.Printf("DeleteEvaluation: failed to delete checker index %s: %v\n", checkerIndexKey, err)
|
|
}
|
|
|
|
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() {
|
|
evalId, err := lastKeySegment(iter.Key())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
eval, err := s.GetEvaluation(evalId)
|
|
if err != nil {
|
|
// Primary record already gone; just clean up this index entry
|
|
// and attempt to clean up the plan index (best-effort scan).
|
|
if err := s.db.Delete(iter.Key()); err != nil {
|
|
return err
|
|
}
|
|
s.deleteEvalPlanIndexByEvalID(evalId)
|
|
continue
|
|
}
|
|
|
|
// Delete plan index if applicable.
|
|
if eval.PlanID != nil {
|
|
planIndexKey := fmt.Sprintf("chckeval-plan|%s|%s", eval.PlanID.String(), eval.Id.String())
|
|
if err := s.db.Delete(planIndexKey); err != nil {
|
|
log.Printf("DeleteEvaluationsByChecker: failed to delete plan index %s: %v\n", planIndexKey, err)
|
|
}
|
|
}
|
|
|
|
// Delete primary record.
|
|
if err := s.db.Delete(fmt.Sprintf("chckeval|%s", eval.Id.String())); err != nil {
|
|
log.Printf("DeleteEvaluationsByChecker: failed to delete primary record %s: %v\n", eval.Id.String(), err)
|
|
}
|
|
|
|
// Delete this checker index entry.
|
|
if err := s.db.Delete(iter.Key()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// deleteEvalPlanIndexByEvalID scans plan indexes to remove any entry for the
|
|
// given evaluation ID. Used when the primary record is already gone and we
|
|
// don't know which plan it belonged to.
|
|
func (s *KVStorage) deleteEvalPlanIndexByEvalID(evalId happydns.Identifier) {
|
|
suffix := "|" + evalId.String()
|
|
iter := s.db.Search("chckeval-plan|")
|
|
defer iter.Release()
|
|
for iter.Next() {
|
|
if strings.HasSuffix(iter.Key(), suffix) {
|
|
if err := s.db.Delete(iter.Key()); err != nil {
|
|
log.Printf("deleteEvalPlanIndexByEvalID: failed to delete %s: %v\n", iter.Key(), err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *KVStorage) TidyEvaluationIndexes() error {
|
|
// Tidy chckeval-plan|{planId}|{evalId} indexes.
|
|
planIter := s.db.Search("chckeval-plan|")
|
|
defer planIter.Release()
|
|
for planIter.Next() {
|
|
key := planIter.Key()
|
|
// Extract planId and evalId from "chckeval-plan|{planId}|{evalId}".
|
|
rest := strings.TrimPrefix(key, "chckeval-plan|")
|
|
parts := strings.SplitN(rest, "|", 2)
|
|
if len(parts) != 2 {
|
|
_ = s.db.Delete(key)
|
|
continue
|
|
}
|
|
|
|
planId, err := happydns.NewIdentifierFromString(parts[0])
|
|
if err != nil {
|
|
_ = s.db.Delete(key)
|
|
continue
|
|
}
|
|
|
|
evalId, err := happydns.NewIdentifierFromString(parts[1])
|
|
if err != nil {
|
|
_ = s.db.Delete(key)
|
|
continue
|
|
}
|
|
|
|
// Check plan exists.
|
|
if _, err := s.GetCheckPlan(planId); err != nil {
|
|
log.Printf("Deleting stale evaluation plan index (plan %s not found): %s\n", parts[0], key)
|
|
_ = s.db.Delete(key)
|
|
continue
|
|
}
|
|
|
|
// Check primary record exists.
|
|
if _, err := s.GetEvaluation(evalId); err != nil {
|
|
log.Printf("Deleting stale evaluation plan index (evaluation %s not found): %s\n", parts[1], key)
|
|
_ = s.db.Delete(key)
|
|
}
|
|
}
|
|
|
|
// Tidy chckeval-chkr|{checkerID}|{target}|{evalId} indexes.
|
|
chkrIter := s.db.Search("chckeval-chkr|")
|
|
defer chkrIter.Release()
|
|
for chkrIter.Next() {
|
|
key := chkrIter.Key()
|
|
// The evalId is the last segment after the last "|".
|
|
lastPipe := strings.LastIndex(key, "|")
|
|
if lastPipe < 0 {
|
|
_ = s.db.Delete(key)
|
|
continue
|
|
}
|
|
evalIdStr := key[lastPipe+1:]
|
|
|
|
evalId, err := happydns.NewIdentifierFromString(evalIdStr)
|
|
if err != nil {
|
|
_ = s.db.Delete(key)
|
|
continue
|
|
}
|
|
|
|
if _, err := s.GetEvaluation(evalId); err != nil {
|
|
log.Printf("Deleting stale evaluation checker index (evaluation %s not found): %s\n", evalIdStr, key)
|
|
_ = s.db.Delete(key)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *KVStorage) ClearEvaluations() error {
|
|
// Delete secondary indexes (chckeval-plan|..., chckeval-chkr|...).
|
|
idxIter := s.db.Search("chckeval-")
|
|
defer idxIter.Release()
|
|
for idxIter.Next() {
|
|
if err := s.db.Delete(idxIter.Key()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Delete primary records (chckeval|...).
|
|
iter, err := s.ListAllEvaluations()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer iter.Close()
|
|
for iter.Next() {
|
|
if err := s.db.Delete(iter.Key()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|