Implement backend model for test results and schedule
This commit is contained in:
parent
cac9419947
commit
5b4dd01a13
16 changed files with 1806 additions and 34 deletions
580
internal/api/controller/testresult_controller.go
Normal file
580
internal/api/controller/testresult_controller.go
Normal file
|
|
@ -0,0 +1,580 @@
|
|||
// 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 controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"maps"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/api/middleware"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// TestResultController handles test result operations
|
||||
type TestResultController struct {
|
||||
scope happydns.TestScopeType
|
||||
testPluginUC happydns.TestPluginUsecase
|
||||
testResultUC happydns.TestResultUsecase
|
||||
testScheduleUC happydns.TestScheduleUsecase
|
||||
testScheduler TestSchedulerInterface
|
||||
}
|
||||
|
||||
// TestSchedulerInterface defines the interface for triggering on-demand tests
|
||||
type TestSchedulerInterface interface {
|
||||
TriggerOnDemandTest(pluginName string, targetType happydns.TestScopeType, targetID happydns.Identifier, userID happydns.Identifier, options happydns.PluginOptions) (happydns.Identifier, error)
|
||||
}
|
||||
|
||||
func NewTestResultController(
|
||||
scope happydns.TestScopeType,
|
||||
testPluginUC happydns.TestPluginUsecase,
|
||||
testResultUC happydns.TestResultUsecase,
|
||||
testScheduleUC happydns.TestScheduleUsecase,
|
||||
testScheduler TestSchedulerInterface,
|
||||
) *TestResultController {
|
||||
return &TestResultController{
|
||||
scope: scope,
|
||||
testPluginUC: testPluginUC,
|
||||
testResultUC: testResultUC,
|
||||
testScheduleUC: testScheduleUC,
|
||||
testScheduler: testScheduler,
|
||||
}
|
||||
}
|
||||
|
||||
// getTargetFromContext extracts the target ID from context based on scope
|
||||
func (tc *TestResultController) getTargetFromContext(c *gin.Context) (happydns.Identifier, error) {
|
||||
switch tc.scope {
|
||||
case happydns.TestScopeUser:
|
||||
user := c.MustGet("user").(*happydns.User)
|
||||
return user.Id, nil
|
||||
case happydns.TestScopeDomain:
|
||||
domain := c.MustGet("domain").(*happydns.Domain)
|
||||
return domain.Id, nil
|
||||
case happydns.TestScopeService:
|
||||
// Services are stored by ID in context
|
||||
serviceID := c.MustGet("serviceid").(happydns.Identifier)
|
||||
return serviceID, nil
|
||||
default:
|
||||
return happydns.Identifier{}, fmt.Errorf("unsupported scope")
|
||||
}
|
||||
}
|
||||
|
||||
// ListAvailableTests lists all available test plugins for the target scope
|
||||
//
|
||||
// @Summary List available tests
|
||||
// @Description Retrieves all available test plugins for the target scope with their last execution status if enabled
|
||||
// @Tags tests
|
||||
// @Produce json
|
||||
// @Param domain path string true "Domain identifier"
|
||||
// @Success 200 {array} object "List of available tests"
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /domains/{domain}/tests [get]
|
||||
func (tc *TestResultController) ListAvailableTests(c *gin.Context) {
|
||||
targetID, err := tc.getTargetFromContext(c)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get all test plugins
|
||||
plugins, err := tc.testPluginUC.ListTestPlugins()
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get schedules for this target
|
||||
schedules, err := tc.testScheduleUC.ListSchedulesByTarget(tc.scope, targetID)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Build schedule map
|
||||
scheduleMap := make(map[string]*happydns.TestSchedule)
|
||||
for _, sched := range schedules {
|
||||
scheduleMap[sched.PluginName] = sched
|
||||
}
|
||||
|
||||
// Build response with last results
|
||||
type TestInfo struct {
|
||||
PluginName string `json:"plugin_name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Schedule *happydns.TestSchedule `json:"schedule,omitempty"`
|
||||
LastResult *happydns.TestResult `json:"last_result,omitempty"`
|
||||
}
|
||||
|
||||
var tests []TestInfo
|
||||
for _, plugin := range plugins {
|
||||
// Get plugin version info
|
||||
versionInfo := plugin.Version()
|
||||
availability := versionInfo.AvailableOn
|
||||
|
||||
// Filter plugins by scope
|
||||
if tc.scope == happydns.TestScopeDomain && !availability.ApplyToDomain {
|
||||
continue
|
||||
}
|
||||
if tc.scope == happydns.TestScopeService && !availability.ApplyToService {
|
||||
continue
|
||||
}
|
||||
|
||||
pluginNames := plugin.PluginEnvName()
|
||||
if len(pluginNames) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
info := TestInfo{
|
||||
PluginName: pluginNames[0],
|
||||
Enabled: false,
|
||||
}
|
||||
|
||||
// Check if there's a schedule
|
||||
if sched, ok := scheduleMap[versionInfo.Name]; ok {
|
||||
info.Enabled = sched.Enabled
|
||||
info.Schedule = sched
|
||||
|
||||
// Get last result
|
||||
results, err := tc.testResultUC.ListTestResultsByTarget(versionInfo.Name, tc.scope, targetID, 1)
|
||||
if err == nil && len(results) > 0 {
|
||||
info.LastResult = results[0]
|
||||
}
|
||||
}
|
||||
|
||||
tests = append(tests, info)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tests)
|
||||
}
|
||||
|
||||
// ListLatestTestResults retrieves the latest test results for a specific plugin
|
||||
//
|
||||
// @Summary Get latest test results
|
||||
// @Description Retrieves the 5 most recent test results for a specific plugin and target
|
||||
// @Tags tests
|
||||
// @Produce json
|
||||
// @Param domain path string true "Domain identifier"
|
||||
// @Param tname path string true "Test plugin name"
|
||||
// @Success 200 {array} happydns.TestResult
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /domains/{domain}/tests/{tname} [get]
|
||||
func (tc *TestResultController) ListLatestTestResults(c *gin.Context) {
|
||||
pluginName := c.Param("tname")
|
||||
targetID, err := tc.getTargetFromContext(c)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
results, err := tc.testResultUC.ListTestResultsByTarget(pluginName, tc.scope, targetID, 5)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, results)
|
||||
}
|
||||
|
||||
// TriggerTest triggers an on-demand test execution
|
||||
//
|
||||
// @Summary Trigger test execution
|
||||
// @Description Triggers an immediate test execution and returns the execution ID
|
||||
// @Tags tests
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param domain path string true "Domain identifier"
|
||||
// @Param tname path string true "Test plugin name"
|
||||
// @Param body body object false "Optional: Plugin options"
|
||||
// @Success 202 {object} object{execution_id=string}
|
||||
// @Failure 400 {object} happydns.ErrorResponse
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /domains/{domain}/tests/{tname} [post]
|
||||
func (tc *TestResultController) TriggerTest(c *gin.Context) {
|
||||
user := middleware.MyUser(c)
|
||||
pluginName := c.Param("tname")
|
||||
targetID, err := tc.getTargetFromContext(c)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse run options
|
||||
var options happydns.SetPluginOptionsRequest
|
||||
if err = c.ShouldBindJSON(&options); err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Merge options with upper levels (user, domain, service)
|
||||
var domainID, serviceID *happydns.Identifier
|
||||
switch tc.scope {
|
||||
case happydns.TestScopeDomain:
|
||||
domainID = &targetID
|
||||
case happydns.TestScopeService:
|
||||
serviceID = &targetID
|
||||
}
|
||||
|
||||
mergedOptions := make(happydns.PluginOptions)
|
||||
|
||||
// Fill opts with default plugin options
|
||||
plugin, err := tc.testPluginUC.GetTestPlugin(pluginName)
|
||||
if err != nil {
|
||||
log.Printf("Warning: unable to get plugin %q for default options: %v", pluginName, err)
|
||||
} else {
|
||||
availableOpts := plugin.AvailableOptions()
|
||||
|
||||
// Collect all option documentation from different scopes
|
||||
allOpts := []happydns.PluginOptionDocumentation{}
|
||||
allOpts = append(allOpts, availableOpts.RunOpts...)
|
||||
allOpts = append(allOpts, availableOpts.ServiceOpts...)
|
||||
allOpts = append(allOpts, availableOpts.DomainOpts...)
|
||||
allOpts = append(allOpts, availableOpts.UserOpts...)
|
||||
allOpts = append(allOpts, availableOpts.AdminOpts...)
|
||||
|
||||
// Fill defaults
|
||||
for _, opt := range allOpts {
|
||||
if opt.Default != nil {
|
||||
mergedOptions[opt.Id] = opt.Default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get merged options from upper levels
|
||||
baseOptions, err := tc.testPluginUC.GetTestPluginOptions(pluginName, &user.Id, domainID, serviceID)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Merge request options on top of base options (request options override)
|
||||
if baseOptions != nil {
|
||||
maps.Copy(mergedOptions, *baseOptions)
|
||||
}
|
||||
maps.Copy(mergedOptions, options.Options)
|
||||
|
||||
// Trigger the test via scheduler (returns error if scheduler is disabled)
|
||||
executionID, err := tc.testScheduler.TriggerOnDemandTest(pluginName, tc.scope, targetID, user.Id, mergedOptions)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusAccepted, gin.H{"execution_id": executionID.String()})
|
||||
}
|
||||
|
||||
// GetTestPluginOptions retrieves plugin options for the target scope
|
||||
//
|
||||
// @Summary Get test plugin options
|
||||
// @Description Retrieves configuration options for a test plugin at the target scope
|
||||
// @Tags tests
|
||||
// @Produce json
|
||||
// @Param domain path string true "Domain identifier"
|
||||
// @Param tname path string true "Test plugin name"
|
||||
// @Success 200 {object} happydns.PluginOptions
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /domains/{domain}/tests/{tname}/options [get]
|
||||
func (tc *TestResultController) GetTestPluginOptions(c *gin.Context) {
|
||||
user := middleware.MyUser(c)
|
||||
pluginName := c.Param("tname")
|
||||
targetID, err := tc.getTargetFromContext(c)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
var domainID, serviceID *happydns.Identifier
|
||||
switch tc.scope {
|
||||
case happydns.TestScopeDomain:
|
||||
domainID = &targetID
|
||||
case happydns.TestScopeService:
|
||||
serviceID = &targetID
|
||||
}
|
||||
|
||||
opts, err := tc.testPluginUC.GetTestPluginOptions(pluginName, &user.Id, domainID, serviceID)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, opts)
|
||||
}
|
||||
|
||||
// AddTestPluginOptions adds or overwrites specific options
|
||||
//
|
||||
// @Summary Add test plugin options
|
||||
// @Description Adds or overwrites specific options for a test plugin at the target scope
|
||||
// @Tags tests
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param domain path string true "Domain identifier"
|
||||
// @Param tname path string true "Test plugin name"
|
||||
// @Param body body happydns.PluginOptions true "Options to add"
|
||||
// @Success 200 {object} bool
|
||||
// @Failure 400 {object} happydns.ErrorResponse
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /domains/{domain}/tests/{tname}/options [post]
|
||||
func (tc *TestResultController) AddTestPluginOptions(c *gin.Context) {
|
||||
user := middleware.MyUser(c)
|
||||
pluginName := c.Param("tname")
|
||||
targetID, err := tc.getTargetFromContext(c)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
var options happydns.PluginOptions
|
||||
if err = c.ShouldBindJSON(&options); err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
var domainID, serviceID *happydns.Identifier
|
||||
switch tc.scope {
|
||||
case happydns.TestScopeDomain:
|
||||
domainID = &targetID
|
||||
case happydns.TestScopeService:
|
||||
serviceID = &targetID
|
||||
}
|
||||
|
||||
err = tc.testPluginUC.OverwriteSomeTestPluginOptions(pluginName, &user.Id, domainID, serviceID, options)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
}
|
||||
|
||||
// ChangeTestPluginOptions replaces all options
|
||||
//
|
||||
// @Summary Replace test plugin options
|
||||
// @Description Replaces all options for a test plugin at the target scope
|
||||
// @Tags tests
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param domain path string true "Domain identifier"
|
||||
// @Param tname path string true "Test plugin name"
|
||||
// @Param body body happydns.PluginOptions true "New complete options"
|
||||
// @Success 200 {object} bool
|
||||
// @Failure 400 {object} happydns.ErrorResponse
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /domains/{domain}/tests/{tname}/options [put]
|
||||
func (tc *TestResultController) ChangeTestPluginOptions(c *gin.Context) {
|
||||
user := middleware.MyUser(c)
|
||||
pluginName := c.Param("tname")
|
||||
targetID, err := tc.getTargetFromContext(c)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
var options happydns.PluginOptions
|
||||
if err = c.ShouldBindJSON(&options); err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
var domainID, serviceID *happydns.Identifier
|
||||
switch tc.scope {
|
||||
case happydns.TestScopeDomain:
|
||||
domainID = &targetID
|
||||
case happydns.TestScopeService:
|
||||
serviceID = &targetID
|
||||
}
|
||||
|
||||
err = tc.testPluginUC.SetTestPluginOptions(pluginName, &user.Id, domainID, serviceID, options)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
}
|
||||
|
||||
// GetTestExecutionStatus retrieves the status of a test execution
|
||||
//
|
||||
// @Summary Get test execution status
|
||||
// @Description Retrieves the current status of a test execution
|
||||
// @Tags tests
|
||||
// @Produce json
|
||||
// @Param domain path string true "Domain identifier"
|
||||
// @Param tname path string true "Test plugin name"
|
||||
// @Param execution_id path string true "Execution ID"
|
||||
// @Success 200 {object} happydns.TestExecution
|
||||
// @Failure 404 {object} happydns.ErrorResponse
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /domains/{domain}/tests/{tname}/executions/{execution_id} [get]
|
||||
func (tc *TestResultController) GetTestExecutionStatus(c *gin.Context) {
|
||||
executionIDStr := c.Param("execution_id")
|
||||
executionID, err := happydns.NewIdentifierFromString(executionIDStr)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, fmt.Errorf("invalid execution ID"))
|
||||
return
|
||||
}
|
||||
|
||||
execution, err := tc.testResultUC.GetTestExecution(executionID)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, execution)
|
||||
}
|
||||
|
||||
// ListTestPluginResults lists all results for a test plugin
|
||||
//
|
||||
// @Summary List test results
|
||||
// @Description Lists all test results for a specific test plugin and target
|
||||
// @Tags tests
|
||||
// @Produce json
|
||||
// @Param domain path string true "Domain identifier"
|
||||
// @Param tname path string true "Test plugin name"
|
||||
// @Param limit query int false "Maximum number of results to return (default: 10)"
|
||||
// @Success 200 {array} happydns.TestResult
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /domains/{domain}/tests/{tname}/results [get]
|
||||
func (tc *TestResultController) ListTestPluginResults(c *gin.Context) {
|
||||
pluginName := c.Param("tname")
|
||||
targetID, err := tc.getTargetFromContext(c)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse limit parameter
|
||||
limit := 10
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
fmt.Sscanf(limitStr, "%d", &limit)
|
||||
}
|
||||
|
||||
results, err := tc.testResultUC.ListTestResultsByTarget(pluginName, tc.scope, targetID, limit)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, results)
|
||||
}
|
||||
|
||||
// DropTestPluginResults deletes all results for a test plugin
|
||||
//
|
||||
// @Summary Delete all test results
|
||||
// @Description Deletes all test results for a specific test plugin and target
|
||||
// @Tags tests
|
||||
// @Produce json
|
||||
// @Param domain path string true "Domain identifier"
|
||||
// @Param tname path string true "Test plugin name"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /domains/{domain}/tests/{tname}/results [delete]
|
||||
func (tc *TestResultController) DropTestPluginResults(c *gin.Context) {
|
||||
pluginName := c.Param("tname")
|
||||
targetID, err := tc.getTargetFromContext(c)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = tc.testResultUC.DeleteAllTestResults(pluginName, tc.scope, targetID)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetTestPluginResult retrieves a specific test result
|
||||
//
|
||||
// @Summary Get test result
|
||||
// @Description Retrieves a specific test result by ID
|
||||
// @Tags tests
|
||||
// @Produce json
|
||||
// @Param domain path string true "Domain identifier"
|
||||
// @Param tname path string true "Test plugin name"
|
||||
// @Param result_id path string true "Result ID"
|
||||
// @Success 200 {object} happydns.TestResult
|
||||
// @Failure 404 {object} happydns.ErrorResponse
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /domains/{domain}/tests/{tname}/results/{result_id} [get]
|
||||
func (tc *TestResultController) GetTestPluginResult(c *gin.Context) {
|
||||
pluginName := c.Param("tname")
|
||||
resultIDStr := c.Param("result_id")
|
||||
targetID, err := tc.getTargetFromContext(c)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
resultID, err := happydns.NewIdentifierFromString(resultIDStr)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, fmt.Errorf("invalid result ID"))
|
||||
return
|
||||
}
|
||||
|
||||
result, err := tc.testResultUC.GetTestResult(pluginName, tc.scope, targetID, resultID)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// DropTestPluginResult deletes a specific test result
|
||||
//
|
||||
// @Summary Delete test result
|
||||
// @Description Deletes a specific test result by ID
|
||||
// @Tags tests
|
||||
// @Produce json
|
||||
// @Param domain path string true "Domain identifier"
|
||||
// @Param tname path string true "Test plugin name"
|
||||
// @Param result_id path string true "Result ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 404 {object} happydns.ErrorResponse
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
// @Router /domains/{domain}/tests/{tname}/results/{result_id} [delete]
|
||||
func (tc *TestResultController) DropTestPluginResult(c *gin.Context) {
|
||||
pluginName := c.Param("tname")
|
||||
resultIDStr := c.Param("result_id")
|
||||
targetID, err := tc.getTargetFromContext(c)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
resultID, err := happydns.NewIdentifierFromString(resultIDStr)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusBadRequest, fmt.Errorf("invalid result ID"))
|
||||
return
|
||||
}
|
||||
|
||||
err = tc.testResultUC.DeleteTestResult(pluginName, tc.scope, targetID, resultID)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
@ -48,6 +48,10 @@ func DeclareDomainRoutes(router *gin.RouterGroup, dependancies happydns.UsecaseD
|
|||
|
||||
DeclareDomainLogRoutes(apiDomainsRoutes, dependancies)
|
||||
|
||||
// Declare test result routes for domain scope
|
||||
|
||||
DeclareScopedTestResultRoutes(apiDomainsRoutes, dependancies, happydns.TestScopeDomain)
|
||||
|
||||
apiDomainsRoutes.POST("/zone", dc.ImportZone)
|
||||
apiDomainsRoutes.POST("/retrieve_zone", dc.RetrieveZone)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,4 +40,7 @@ func DeclareZoneServiceRoutes(apiZonesRoutes, apiZonesSubdomainRoutes *gin.Route
|
|||
apiZonesSubdomainServiceIdRoutes.Use(middleware.ServiceIdHandler(dependancies.ServiceUsecase()))
|
||||
apiZonesSubdomainServiceIdRoutes.GET("", sc.GetZoneService)
|
||||
apiZonesSubdomainServiceIdRoutes.DELETE("", sc.DeleteZoneService)
|
||||
|
||||
// Declare test result routes for service scope
|
||||
DeclareScopedTestResultRoutes(apiZonesSubdomainServiceIdRoutes, dependancies, happydns.TestScopeService)
|
||||
}
|
||||
|
|
|
|||
103
internal/api/route/testresults.go
Normal file
103
internal/api/route/testresults.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// 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 route
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/api/controller"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// testSchedulerProvider is an interface for getting the test scheduler from dependencies
|
||||
type testSchedulerProvider interface {
|
||||
TestScheduler() controller.TestSchedulerInterface
|
||||
}
|
||||
|
||||
// DeclareScopedTestResultRoutes declares test result routes for a specific scope (domain, zone, or service)
|
||||
func DeclareScopedTestResultRoutes(
|
||||
scopedRouter *gin.RouterGroup,
|
||||
dependancies happydns.UsecaseDependancies,
|
||||
scope happydns.TestScopeType,
|
||||
) {
|
||||
testScheduler := dependancies.(testSchedulerProvider).TestScheduler()
|
||||
|
||||
tc := controller.NewTestResultController(
|
||||
scope,
|
||||
dependancies.TestPluginUsecase(),
|
||||
dependancies.TestResultUsecase(),
|
||||
dependancies.TestScheduleUsecase(),
|
||||
testScheduler,
|
||||
)
|
||||
|
||||
// List all available tests with their status
|
||||
scopedRouter.GET("/tests", tc.ListAvailableTests)
|
||||
|
||||
// Test-specific routes
|
||||
apiTestsRoutes := scopedRouter.Group("/tests/:tname")
|
||||
{
|
||||
// Get latest results for a test
|
||||
apiTestsRoutes.GET("", tc.ListLatestTestResults)
|
||||
|
||||
// Trigger an on-demand test
|
||||
apiTestsRoutes.POST("", tc.TriggerTest)
|
||||
|
||||
// Manage test plugin options at this scope
|
||||
apiTestsRoutes.GET("/options", tc.GetTestPluginOptions)
|
||||
apiTestsRoutes.POST("/options", tc.AddTestPluginOptions)
|
||||
apiTestsRoutes.PUT("/options", tc.ChangeTestPluginOptions)
|
||||
|
||||
// Test execution routes
|
||||
apiTestExecutionsRoutes := apiTestsRoutes.Group("/executions/:execution_id")
|
||||
{
|
||||
apiTestExecutionsRoutes.GET("", tc.GetTestExecutionStatus)
|
||||
}
|
||||
|
||||
// Test results routes
|
||||
apiTestsRoutes.GET("/results", tc.ListTestPluginResults)
|
||||
apiTestsRoutes.DELETE("/results", tc.DropTestPluginResults)
|
||||
|
||||
apiTestResultsRoutes := apiTestsRoutes.Group("/results/:result_id")
|
||||
{
|
||||
apiTestResultsRoutes.GET("", tc.GetTestPluginResult)
|
||||
apiTestResultsRoutes.DELETE("", tc.DropTestPluginResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeclareTestScheduleRoutes declares test schedule management routes
|
||||
func DeclareTestScheduleRoutes(router *gin.RouterGroup, dependancies happydns.UsecaseDependancies) {
|
||||
sc := controller.NewTestScheduleController(dependancies.TestScheduleUsecase())
|
||||
|
||||
schedulesRoutes := router.Group("/plugins/tests/schedules")
|
||||
{
|
||||
schedulesRoutes.GET("", sc.ListTestSchedules)
|
||||
schedulesRoutes.POST("", sc.CreateTestSchedule)
|
||||
|
||||
scheduleRoutes := schedulesRoutes.Group("/:schedule_id")
|
||||
{
|
||||
scheduleRoutes.GET("", sc.GetTestSchedule)
|
||||
scheduleRoutes.PUT("", sc.UpdateTestSchedule)
|
||||
scheduleRoutes.DELETE("", sc.DeleteTestSchedule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
|
||||
api "git.happydns.org/happyDomain/internal/api/route"
|
||||
"git.happydns.org/happyDomain/internal/api/controller"
|
||||
"git.happydns.org/happyDomain/internal/mailer"
|
||||
"git.happydns.org/happyDomain/internal/newsletter"
|
||||
"git.happydns.org/happyDomain/internal/session"
|
||||
|
|
@ -44,6 +45,7 @@ import (
|
|||
providerUC "git.happydns.org/happyDomain/internal/usecase/provider"
|
||||
serviceUC "git.happydns.org/happyDomain/internal/usecase/service"
|
||||
sessionUC "git.happydns.org/happyDomain/internal/usecase/session"
|
||||
testresultUC "git.happydns.org/happyDomain/internal/usecase/testresult"
|
||||
userUC "git.happydns.org/happyDomain/internal/usecase/user"
|
||||
zoneUC "git.happydns.org/happyDomain/internal/usecase/zone"
|
||||
zoneServiceUC "git.happydns.org/happyDomain/internal/usecase/zone_service"
|
||||
|
|
@ -65,6 +67,7 @@ type Usecases struct {
|
|||
service happydns.ServiceUsecase
|
||||
serviceSpecs happydns.ServiceSpecsUsecase
|
||||
testPlugin happydns.TestPluginUsecase
|
||||
testResult happydns.TestResultUsecase
|
||||
user happydns.UserUsecase
|
||||
zone happydns.ZoneUsecase
|
||||
zoneService happydns.ZoneServiceUsecase
|
||||
|
|
@ -73,15 +76,15 @@ type Usecases struct {
|
|||
}
|
||||
|
||||
type App struct {
|
||||
cfg *happydns.Options
|
||||
mailer *mailer.Mailer
|
||||
newsletter happydns.NewsletterSubscriptor
|
||||
router *gin.Engine
|
||||
srv *http.Server
|
||||
insights *insightsCollector
|
||||
plugins happydns.PluginManager
|
||||
store storage.Storage
|
||||
usecases Usecases
|
||||
cfg *happydns.Options
|
||||
mailer *mailer.Mailer
|
||||
newsletter happydns.NewsletterSubscriptor
|
||||
router *gin.Engine
|
||||
srv *http.Server
|
||||
insights *insightsCollector
|
||||
plugins happydns.PluginManager
|
||||
store storage.Storage
|
||||
usecases Usecases
|
||||
}
|
||||
|
||||
func (a *App) AuthenticationUsecase() happydns.AuthenticationUsecase {
|
||||
|
|
@ -144,6 +147,10 @@ func (a *App) TestPluginUsecase() happydns.TestPluginUsecase {
|
|||
return a.usecases.testPlugin
|
||||
}
|
||||
|
||||
func (a *App) TestResultUsecase() happydns.TestResultUsecase {
|
||||
return a.usecases.testResult
|
||||
}
|
||||
|
||||
func (a *App) UserUsecase() happydns.UserUsecase {
|
||||
return a.usecases.user
|
||||
}
|
||||
|
|
@ -280,6 +287,7 @@ func (app *App) initUsecases() {
|
|||
app.usecases.resolver = usecase.NewResolverUsecase(app.cfg)
|
||||
app.usecases.session = sessionService
|
||||
app.usecases.testPlugin = pluginUC.NewTestPluginUsecase(app.cfg, app.plugins, app.store)
|
||||
app.usecases.testResult = testresultUC.NewTestResultUsecase(app.store, app.cfg)
|
||||
|
||||
app.usecases.orchestrator = orchestrator.NewOrchestrator(
|
||||
domainLogService,
|
||||
|
|
|
|||
|
|
@ -44,15 +44,17 @@ func ConsolidateConfig() (opts *happydns.Options, err error) {
|
|||
|
||||
// Define defaults options
|
||||
opts = &happydns.Options{
|
||||
AdminBind: "./happydomain.sock",
|
||||
BasePath: "/",
|
||||
Bind: ":8081",
|
||||
DefaultNameServer: "127.0.0.1:53",
|
||||
ExternalURL: *u,
|
||||
JWTSigningMethod: "HS512",
|
||||
MailFrom: mail.Address{Name: "happyDomain", Address: "happydomain@localhost"},
|
||||
MailSMTPPort: 587,
|
||||
StorageEngine: "leveldb",
|
||||
AdminBind: "./happydomain.sock",
|
||||
BasePath: "/",
|
||||
Bind: ":8081",
|
||||
DefaultNameServer: "127.0.0.1:53",
|
||||
ExternalURL: *u,
|
||||
JWTSigningMethod: "HS512",
|
||||
MailFrom: mail.Address{Name: "happyDomain", Address: "happydomain@localhost"},
|
||||
MailSMTPPort: 587,
|
||||
StorageEngine: "leveldb",
|
||||
MaxResultsPerTest: 100,
|
||||
ResultRetentionDays: 90,
|
||||
}
|
||||
|
||||
declareFlags(opts)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import (
|
|||
"git.happydns.org/happyDomain/internal/usecase/plugin"
|
||||
"git.happydns.org/happyDomain/internal/usecase/provider"
|
||||
"git.happydns.org/happyDomain/internal/usecase/session"
|
||||
"git.happydns.org/happyDomain/internal/usecase/testresult"
|
||||
"git.happydns.org/happyDomain/internal/usecase/user"
|
||||
"git.happydns.org/happyDomain/internal/usecase/zone"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
|
|
@ -47,6 +48,7 @@ type Storage interface {
|
|||
plugin.PluginStorage
|
||||
provider.ProviderStorage
|
||||
session.SessionStorage
|
||||
testresult.TestResultStorage
|
||||
user.UserStorage
|
||||
zone.ZoneStorage
|
||||
|
||||
|
|
|
|||
433
internal/storage/kvtpl/testresult.go
Normal file
433
internal/storage/kvtpl/testresult.go
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
// 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 database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// Test Result storage keys:
|
||||
// testresult|{plugin-name}|{target-type}|{target-id}|{result-id}
|
||||
func makeTestResultKey(pluginName string, targetType happydns.TestScopeType, targetId, resultId happydns.Identifier) string {
|
||||
return fmt.Sprintf("testresult|%s|%d|%s|%s", pluginName, targetType, targetId.String(), resultId.String())
|
||||
}
|
||||
|
||||
func makeTestResultPrefix(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier) string {
|
||||
return fmt.Sprintf("testresult|%s|%d|%s|", pluginName, targetType, targetId.String())
|
||||
}
|
||||
|
||||
// ListTestResults retrieves test results for a specific plugin+target combination
|
||||
func (s *KVStorage) ListTestResults(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, limit int) ([]*happydns.TestResult, error) {
|
||||
prefix := makeTestResultPrefix(pluginName, targetType, targetId)
|
||||
iter := s.db.Search(prefix)
|
||||
defer iter.Release()
|
||||
|
||||
var results []*happydns.TestResult
|
||||
for iter.Next() {
|
||||
var r happydns.TestResult
|
||||
if err := s.db.DecodeData(iter.Value(), &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, &r)
|
||||
}
|
||||
|
||||
// Sort by ExecutedAt descending (most recent first)
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].ExecutedAt.After(results[j].ExecutedAt)
|
||||
})
|
||||
|
||||
// Apply limit
|
||||
if limit > 0 && len(results) > limit {
|
||||
results = results[:limit]
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// ListTestResultsByPlugin retrieves all test results for a plugin across all targets for a user
|
||||
func (s *KVStorage) ListTestResultsByPlugin(userId happydns.Identifier, pluginName string, limit int) ([]*happydns.TestResult, error) {
|
||||
prefix := fmt.Sprintf("testresult|%s|", pluginName)
|
||||
iter := s.db.Search(prefix)
|
||||
defer iter.Release()
|
||||
|
||||
var results []*happydns.TestResult
|
||||
for iter.Next() {
|
||||
var r happydns.TestResult
|
||||
if err := s.db.DecodeData(iter.Value(), &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Filter by user
|
||||
if r.OwnerId.Equals(userId) {
|
||||
results = append(results, &r)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by ExecutedAt descending (most recent first)
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].ExecutedAt.After(results[j].ExecutedAt)
|
||||
})
|
||||
|
||||
// Apply limit
|
||||
if limit > 0 && len(results) > limit {
|
||||
results = results[:limit]
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// ListTestResultsByUser retrieves all test results for a user
|
||||
func (s *KVStorage) ListTestResultsByUser(userId happydns.Identifier, limit int) ([]*happydns.TestResult, error) {
|
||||
iter := s.db.Search("testresult|")
|
||||
defer iter.Release()
|
||||
|
||||
var results []*happydns.TestResult
|
||||
for iter.Next() {
|
||||
var r happydns.TestResult
|
||||
if err := s.db.DecodeData(iter.Value(), &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Filter by user
|
||||
if r.OwnerId.Equals(userId) {
|
||||
results = append(results, &r)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by ExecutedAt descending (most recent first)
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].ExecutedAt.After(results[j].ExecutedAt)
|
||||
})
|
||||
|
||||
// Apply limit
|
||||
if limit > 0 && len(results) > limit {
|
||||
results = results[:limit]
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetTestResult retrieves a specific test result by its ID
|
||||
func (s *KVStorage) GetTestResult(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, resultId happydns.Identifier) (*happydns.TestResult, error) {
|
||||
key := makeTestResultKey(pluginName, targetType, targetId, resultId)
|
||||
var result happydns.TestResult
|
||||
err := s.db.Get(key, &result)
|
||||
if errors.Is(err, happydns.ErrNotFound) {
|
||||
return nil, happydns.ErrTestResultNotFound
|
||||
}
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// CreateTestResult stores a new test result
|
||||
func (s *KVStorage) CreateTestResult(result *happydns.TestResult) error {
|
||||
prefix := makeTestResultPrefix(result.PluginName, result.TestType, result.TargetId)
|
||||
key, id, err := s.db.FindIdentifierKey(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.Id = id
|
||||
return s.db.Put(key, result)
|
||||
}
|
||||
|
||||
// DeleteTestResult removes a specific test result
|
||||
func (s *KVStorage) DeleteTestResult(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, resultId happydns.Identifier) error {
|
||||
key := makeTestResultKey(pluginName, targetType, targetId, resultId)
|
||||
return s.db.Delete(key)
|
||||
}
|
||||
|
||||
// DeleteOldTestResults removes old test results keeping only the most recent N results
|
||||
func (s *KVStorage) DeleteOldTestResults(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, keepCount int) error {
|
||||
results, err := s.ListTestResults(pluginName, targetType, targetId, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Results are already sorted by ExecutedAt descending
|
||||
// Delete results beyond keepCount
|
||||
if len(results) > keepCount {
|
||||
for _, r := range results[keepCount:] {
|
||||
if err := s.DeleteTestResult(pluginName, targetType, targetId, r.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test Schedule storage keys:
|
||||
// testschedule|{schedule-id}
|
||||
// testschedule.byuser|{user-id}|{schedule-id}
|
||||
// testschedule.bytarget|{target-type}|{target-id}|{schedule-id}
|
||||
|
||||
func makeTestScheduleKey(scheduleId happydns.Identifier) string {
|
||||
return fmt.Sprintf("testschedule|%s", scheduleId.String())
|
||||
}
|
||||
|
||||
func makeTestScheduleUserIndexKey(userId, scheduleId happydns.Identifier) string {
|
||||
return fmt.Sprintf("testschedule.byuser|%s|%s", userId.String(), scheduleId.String())
|
||||
}
|
||||
|
||||
func makeTestScheduleTargetIndexKey(targetType happydns.TestScopeType, targetId, scheduleId happydns.Identifier) string {
|
||||
return fmt.Sprintf("testschedule.bytarget|%d|%s|%s", targetType, targetId.String(), scheduleId.String())
|
||||
}
|
||||
|
||||
// ListEnabledTestSchedules retrieves all enabled schedules
|
||||
func (s *KVStorage) ListEnabledTestSchedules() ([]*happydns.TestSchedule, error) {
|
||||
iter := s.db.Search("testschedule|")
|
||||
defer iter.Release()
|
||||
|
||||
var schedules []*happydns.TestSchedule
|
||||
for iter.Next() {
|
||||
var sched happydns.TestSchedule
|
||||
if err := s.db.DecodeData(iter.Value(), &sched); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sched.Enabled {
|
||||
schedules = append(schedules, &sched)
|
||||
}
|
||||
}
|
||||
|
||||
return schedules, nil
|
||||
}
|
||||
|
||||
// ListTestSchedulesByUser retrieves all schedules for a specific user
|
||||
func (s *KVStorage) ListTestSchedulesByUser(userId happydns.Identifier) ([]*happydns.TestSchedule, error) {
|
||||
prefix := fmt.Sprintf("testschedule.byuser|%s|", userId.String())
|
||||
iter := s.db.Search(prefix)
|
||||
defer iter.Release()
|
||||
|
||||
var schedules []*happydns.TestSchedule
|
||||
for iter.Next() {
|
||||
// Extract schedule ID from index key
|
||||
key := string(iter.Key())
|
||||
parts := strings.Split(key, "|")
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
scheduleId, err := happydns.NewIdentifierFromString(parts[2])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the actual schedule
|
||||
var sched happydns.TestSchedule
|
||||
schedKey := makeTestScheduleKey(scheduleId)
|
||||
if err := s.db.Get(schedKey, &sched); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
schedules = append(schedules, &sched)
|
||||
}
|
||||
|
||||
return schedules, nil
|
||||
}
|
||||
|
||||
// ListTestSchedulesByTarget retrieves all schedules for a specific target
|
||||
func (s *KVStorage) ListTestSchedulesByTarget(targetType happydns.TestScopeType, targetId happydns.Identifier) ([]*happydns.TestSchedule, error) {
|
||||
prefix := fmt.Sprintf("testschedule.bytarget|%d|%s|", targetType, targetId.String())
|
||||
iter := s.db.Search(prefix)
|
||||
defer iter.Release()
|
||||
|
||||
var schedules []*happydns.TestSchedule
|
||||
for iter.Next() {
|
||||
// Extract schedule ID from index key
|
||||
key := string(iter.Key())
|
||||
parts := strings.Split(key, "|")
|
||||
if len(parts) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
scheduleId, err := happydns.NewIdentifierFromString(parts[3])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the actual schedule
|
||||
var sched happydns.TestSchedule
|
||||
schedKey := makeTestScheduleKey(scheduleId)
|
||||
if err := s.db.Get(schedKey, &sched); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
schedules = append(schedules, &sched)
|
||||
}
|
||||
|
||||
return schedules, nil
|
||||
}
|
||||
|
||||
// GetTestSchedule retrieves a specific schedule by ID
|
||||
func (s *KVStorage) GetTestSchedule(scheduleId happydns.Identifier) (*happydns.TestSchedule, error) {
|
||||
key := makeTestScheduleKey(scheduleId)
|
||||
var schedule happydns.TestSchedule
|
||||
err := s.db.Get(key, &schedule)
|
||||
if errors.Is(err, happydns.ErrNotFound) {
|
||||
return nil, happydns.ErrTestScheduleNotFound
|
||||
}
|
||||
return &schedule, err
|
||||
}
|
||||
|
||||
// CreateTestSchedule creates a new test schedule
|
||||
func (s *KVStorage) CreateTestSchedule(schedule *happydns.TestSchedule) error {
|
||||
key, id, err := s.db.FindIdentifierKey("testschedule|")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schedule.Id = id
|
||||
|
||||
// Store the schedule
|
||||
if err := s.db.Put(key, schedule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create indexes
|
||||
userIndexKey := makeTestScheduleUserIndexKey(schedule.OwnerId, schedule.Id)
|
||||
if err := s.db.Put(userIndexKey, []byte{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetIndexKey := makeTestScheduleTargetIndexKey(schedule.TargetType, schedule.TargetId, schedule.Id)
|
||||
if err := s.db.Put(targetIndexKey, []byte{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTestSchedule updates an existing schedule
|
||||
func (s *KVStorage) UpdateTestSchedule(schedule *happydns.TestSchedule) error {
|
||||
key := makeTestScheduleKey(schedule.Id)
|
||||
return s.db.Put(key, schedule)
|
||||
}
|
||||
|
||||
// DeleteTestSchedule removes a schedule and its indexes
|
||||
func (s *KVStorage) DeleteTestSchedule(scheduleId happydns.Identifier) error {
|
||||
// Get the schedule first to know what indexes to delete
|
||||
schedule, err := s.GetTestSchedule(scheduleId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete indexes
|
||||
userIndexKey := makeTestScheduleUserIndexKey(schedule.OwnerId, schedule.Id)
|
||||
if err := s.db.Delete(userIndexKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetIndexKey := makeTestScheduleTargetIndexKey(schedule.TargetType, schedule.TargetId, schedule.Id)
|
||||
if err := s.db.Delete(targetIndexKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the schedule itself
|
||||
key := makeTestScheduleKey(scheduleId)
|
||||
return s.db.Delete(key)
|
||||
}
|
||||
|
||||
// Test Execution storage keys:
|
||||
// testexec|{execution-id}
|
||||
|
||||
func makeTestExecutionKey(executionId happydns.Identifier) string {
|
||||
return fmt.Sprintf("testexec|%s", executionId.String())
|
||||
}
|
||||
|
||||
// ListActiveTestExecutions retrieves all executions that are pending or running
|
||||
func (s *KVStorage) ListActiveTestExecutions() ([]*happydns.TestExecution, error) {
|
||||
iter := s.db.Search("testexec|")
|
||||
defer iter.Release()
|
||||
|
||||
var executions []*happydns.TestExecution
|
||||
for iter.Next() {
|
||||
var exec happydns.TestExecution
|
||||
if err := s.db.DecodeData(iter.Value(), &exec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exec.Status == happydns.TestExecutionPending || exec.Status == happydns.TestExecutionRunning {
|
||||
executions = append(executions, &exec)
|
||||
}
|
||||
}
|
||||
|
||||
return executions, nil
|
||||
}
|
||||
|
||||
// GetTestExecution retrieves a specific execution by ID
|
||||
func (s *KVStorage) GetTestExecution(executionId happydns.Identifier) (*happydns.TestExecution, error) {
|
||||
key := makeTestExecutionKey(executionId)
|
||||
var execution happydns.TestExecution
|
||||
err := s.db.Get(key, &execution)
|
||||
if errors.Is(err, happydns.ErrNotFound) {
|
||||
return nil, happydns.ErrTestExecutionNotFound
|
||||
}
|
||||
return &execution, err
|
||||
}
|
||||
|
||||
// CreateTestExecution creates a new test execution record
|
||||
func (s *KVStorage) CreateTestExecution(execution *happydns.TestExecution) error {
|
||||
key, id, err := s.db.FindIdentifierKey("testexec|")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
execution.Id = id
|
||||
return s.db.Put(key, execution)
|
||||
}
|
||||
|
||||
// UpdateTestExecution updates an existing execution record
|
||||
func (s *KVStorage) UpdateTestExecution(execution *happydns.TestExecution) error {
|
||||
key := makeTestExecutionKey(execution.Id)
|
||||
return s.db.Put(key, execution)
|
||||
}
|
||||
|
||||
// DeleteTestExecution removes an execution record
|
||||
func (s *KVStorage) DeleteTestExecution(executionId happydns.Identifier) error {
|
||||
key := makeTestExecutionKey(executionId)
|
||||
return s.db.Delete(key)
|
||||
}
|
||||
|
||||
// Scheduler state storage key:
|
||||
// testscheduler.lastrun
|
||||
|
||||
// TestSchedulerRun marks that the scheduler has run at current time
|
||||
func (s *KVStorage) TestSchedulerRun() error {
|
||||
now := time.Now()
|
||||
return s.db.Put("testscheduler.lastrun", &now)
|
||||
}
|
||||
|
||||
// LastTestSchedulerRun retrieves the last time the scheduler ran
|
||||
func (s *KVStorage) LastTestSchedulerRun() (*time.Time, error) {
|
||||
var lastRun time.Time
|
||||
err := s.db.Get("testscheduler.lastrun", &lastRun)
|
||||
if errors.Is(err, happydns.ErrNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &lastRun, nil
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ package plugin
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"sort"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
|
|
@ -103,9 +104,7 @@ func (tu *testPluginUsecase) GetTestPluginOptions(pname string, userid *happydns
|
|||
opts := make(happydns.PluginOptions)
|
||||
|
||||
for _, c := range configs {
|
||||
for k, v := range c.Options {
|
||||
opts[k] = v
|
||||
}
|
||||
maps.Copy(opts, c.Options)
|
||||
}
|
||||
|
||||
return &opts, nil
|
||||
|
|
@ -116,7 +115,40 @@ func (tu *testPluginUsecase) ListTestPlugins() ([]happydns.TestPlugin, error) {
|
|||
}
|
||||
|
||||
func (tu *testPluginUsecase) SetTestPluginOptions(pname string, userid *happydns.Identifier, domainid *happydns.Identifier, serviceid *happydns.Identifier, opts happydns.PluginOptions) error {
|
||||
return tu.store.UpdatePluginConfiguration(pname, userid, domainid, serviceid, opts)
|
||||
// filter opts that correspond to the level set
|
||||
plugin, err := tu.GetTestPlugin(pname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get test plugin: %w", err)
|
||||
}
|
||||
|
||||
var optNames []string
|
||||
if serviceid != nil {
|
||||
for _, opt := range plugin.AvailableOptions().ServiceOpts {
|
||||
optNames = append(optNames, opt.Id)
|
||||
}
|
||||
} else if domainid != nil {
|
||||
for _, opt := range plugin.AvailableOptions().DomainOpts {
|
||||
optNames = append(optNames, opt.Id)
|
||||
}
|
||||
} else if userid != nil {
|
||||
for _, opt := range plugin.AvailableOptions().UserOpts {
|
||||
optNames = append(optNames, opt.Id)
|
||||
}
|
||||
} else {
|
||||
for _, opt := range plugin.AvailableOptions().AdminOpts {
|
||||
optNames = append(optNames, opt.Id)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter opts to only include keys that are in optNames
|
||||
filteredOpts := make(happydns.PluginOptions)
|
||||
for _, optName := range optNames {
|
||||
if val, exists := opts[optName]; exists && val != "" {
|
||||
filteredOpts[optName] = val
|
||||
}
|
||||
}
|
||||
|
||||
return tu.store.UpdatePluginConfiguration(pname, userid, domainid, serviceid, filteredOpts)
|
||||
}
|
||||
|
||||
func (tu *testPluginUsecase) OverwriteSomeTestPluginOptions(pname string, userid *happydns.Identifier, domainid *happydns.Identifier, serviceid *happydns.Identifier, opts happydns.PluginOptions) error {
|
||||
|
|
@ -125,9 +157,7 @@ func (tu *testPluginUsecase) OverwriteSomeTestPluginOptions(pname string, userid
|
|||
return err
|
||||
}
|
||||
|
||||
for k, v := range opts {
|
||||
(*current)[k] = v
|
||||
}
|
||||
maps.Copy(*current, opts)
|
||||
|
||||
return tu.store.UpdatePluginConfiguration(pname, userid, domainid, serviceid, *current)
|
||||
}
|
||||
|
|
|
|||
98
internal/usecase/testresult/storage.go
Normal file
98
internal/usecase/testresult/storage.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// 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 testresult
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// TestResultStorage defines the storage interface for test results and related data
|
||||
type TestResultStorage interface {
|
||||
// Test Results
|
||||
// ListTestResults retrieves test results for a specific plugin+target combination
|
||||
ListTestResults(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, limit int) ([]*happydns.TestResult, error)
|
||||
|
||||
// ListTestResultsByPlugin retrieves all test results for a plugin across all targets for a user
|
||||
ListTestResultsByPlugin(userId happydns.Identifier, pluginName string, limit int) ([]*happydns.TestResult, error)
|
||||
|
||||
// ListTestResultsByUser retrieves all test results for a user
|
||||
ListTestResultsByUser(userId happydns.Identifier, limit int) ([]*happydns.TestResult, error)
|
||||
|
||||
// GetTestResult retrieves a specific test result by its ID
|
||||
GetTestResult(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, resultId happydns.Identifier) (*happydns.TestResult, error)
|
||||
|
||||
// CreateTestResult stores a new test result
|
||||
CreateTestResult(result *happydns.TestResult) error
|
||||
|
||||
// DeleteTestResult removes a specific test result
|
||||
DeleteTestResult(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, resultId happydns.Identifier) error
|
||||
|
||||
// DeleteOldTestResults removes old test results keeping only the most recent N results
|
||||
DeleteOldTestResults(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, keepCount int) error
|
||||
|
||||
// Test Schedules
|
||||
// ListEnabledTestSchedules retrieves all enabled schedules (for scheduler)
|
||||
ListEnabledTestSchedules() ([]*happydns.TestSchedule, error)
|
||||
|
||||
// ListTestSchedulesByUser retrieves all schedules for a specific user
|
||||
ListTestSchedulesByUser(userId happydns.Identifier) ([]*happydns.TestSchedule, error)
|
||||
|
||||
// ListTestSchedulesByTarget retrieves all schedules for a specific target
|
||||
ListTestSchedulesByTarget(targetType happydns.TestScopeType, targetId happydns.Identifier) ([]*happydns.TestSchedule, error)
|
||||
|
||||
// GetTestSchedule retrieves a specific schedule by ID
|
||||
GetTestSchedule(scheduleId happydns.Identifier) (*happydns.TestSchedule, error)
|
||||
|
||||
// CreateTestSchedule creates a new test schedule
|
||||
CreateTestSchedule(schedule *happydns.TestSchedule) error
|
||||
|
||||
// UpdateTestSchedule updates an existing schedule
|
||||
UpdateTestSchedule(schedule *happydns.TestSchedule) error
|
||||
|
||||
// DeleteTestSchedule removes a schedule
|
||||
DeleteTestSchedule(scheduleId happydns.Identifier) error
|
||||
|
||||
// Test Executions
|
||||
// ListActiveTestExecutions retrieves all executions that are pending or running
|
||||
ListActiveTestExecutions() ([]*happydns.TestExecution, error)
|
||||
|
||||
// GetTestExecution retrieves a specific execution by ID
|
||||
GetTestExecution(executionId happydns.Identifier) (*happydns.TestExecution, error)
|
||||
|
||||
// CreateTestExecution creates a new test execution record
|
||||
CreateTestExecution(execution *happydns.TestExecution) error
|
||||
|
||||
// UpdateTestExecution updates an existing execution record
|
||||
UpdateTestExecution(execution *happydns.TestExecution) error
|
||||
|
||||
// DeleteTestExecution removes an execution record
|
||||
DeleteTestExecution(executionId happydns.Identifier) error
|
||||
|
||||
// Scheduler State
|
||||
// TestSchedulerRun marks that the scheduler has run at current time
|
||||
TestSchedulerRun() error
|
||||
|
||||
// LastTestSchedulerRun retrieves the last time the scheduler ran
|
||||
LastTestSchedulerRun() (*time.Time, error)
|
||||
}
|
||||
222
internal/usecase/testresult/testresult_usecase.go
Normal file
222
internal/usecase/testresult/testresult_usecase.go
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
// 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 testresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// TestResultUsecase implements business logic for test results
|
||||
type TestResultUsecase struct {
|
||||
storage TestResultStorage
|
||||
options *happydns.Options
|
||||
}
|
||||
|
||||
// NewTestResultUsecase creates a new test result usecase
|
||||
func NewTestResultUsecase(storage TestResultStorage, options *happydns.Options) *TestResultUsecase {
|
||||
return &TestResultUsecase{
|
||||
storage: storage,
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// ListTestResultsByTarget retrieves test results for a specific target
|
||||
func (u *TestResultUsecase) ListTestResultsByTarget(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, limit int) ([]*happydns.TestResult, error) {
|
||||
// Apply default limit if not specified
|
||||
if limit <= 0 {
|
||||
limit = 5 // Default to 5 most recent results
|
||||
}
|
||||
|
||||
return u.storage.ListTestResults(pluginName, targetType, targetId, limit)
|
||||
}
|
||||
|
||||
// ListAllTestResultsByTarget retrieves all test results for a target across all plugins
|
||||
func (u *TestResultUsecase) ListAllTestResultsByTarget(targetType happydns.TestScopeType, targetId happydns.Identifier, userId happydns.Identifier, limit int) ([]*happydns.TestResult, error) {
|
||||
// Get all results for the user and filter by target
|
||||
allResults, err := u.storage.ListTestResultsByUser(userId, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter by target
|
||||
var results []*happydns.TestResult
|
||||
for _, r := range allResults {
|
||||
if r.TestType == targetType && r.TargetId.Equals(targetId) {
|
||||
results = append(results, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply limit
|
||||
if limit > 0 && len(results) > limit {
|
||||
results = results[:limit]
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetTestResult retrieves a specific test result
|
||||
func (u *TestResultUsecase) GetTestResult(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, resultId happydns.Identifier) (*happydns.TestResult, error) {
|
||||
return u.storage.GetTestResult(pluginName, targetType, targetId, resultId)
|
||||
}
|
||||
|
||||
// CreateTestResult stores a new test result and enforces retention policy
|
||||
func (u *TestResultUsecase) CreateTestResult(result *happydns.TestResult) error {
|
||||
// Store the result
|
||||
if err := u.storage.CreateTestResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Enforce retention policy
|
||||
maxResults := u.options.MaxResultsPerTest
|
||||
if maxResults <= 0 {
|
||||
maxResults = 100 // Default
|
||||
}
|
||||
|
||||
return u.storage.DeleteOldTestResults(result.PluginName, result.TestType, result.TargetId, maxResults)
|
||||
}
|
||||
|
||||
// DeleteTestResult removes a specific test result
|
||||
func (u *TestResultUsecase) DeleteTestResult(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier, resultId happydns.Identifier) error {
|
||||
return u.storage.DeleteTestResult(pluginName, targetType, targetId, resultId)
|
||||
}
|
||||
|
||||
// DeleteAllTestResults removes all results for a specific plugin+target combination
|
||||
func (u *TestResultUsecase) DeleteAllTestResults(pluginName string, targetType happydns.TestScopeType, targetId happydns.Identifier) error {
|
||||
// Get all results first
|
||||
results, err := u.storage.ListTestResults(pluginName, targetType, targetId, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete each result
|
||||
for _, r := range results {
|
||||
if err := u.storage.DeleteTestResult(pluginName, targetType, targetId, r.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupOldResults removes test results older than retention period
|
||||
func (u *TestResultUsecase) CleanupOldResults() error {
|
||||
retentionDays := u.options.ResultRetentionDays
|
||||
if retentionDays <= 0 {
|
||||
retentionDays = 90 // Default
|
||||
}
|
||||
|
||||
cutoffTime := time.Now().AddDate(0, 0, -retentionDays)
|
||||
|
||||
// Get all results for all users (inefficient but necessary without a time-based index)
|
||||
// In a production system, you might want to add a time-based index for this
|
||||
// For now, we'll iterate through results and delete old ones
|
||||
|
||||
// This is a placeholder - the actual implementation would need to be optimized
|
||||
// based on specific storage patterns
|
||||
_ = cutoffTime
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTestExecution retrieves the status of a test execution
|
||||
func (u *TestResultUsecase) GetTestExecution(executionId happydns.Identifier) (*happydns.TestExecution, error) {
|
||||
return u.storage.GetTestExecution(executionId)
|
||||
}
|
||||
|
||||
// CreateTestExecution creates a new test execution record
|
||||
func (u *TestResultUsecase) CreateTestExecution(execution *happydns.TestExecution) error {
|
||||
if execution.StartedAt.IsZero() {
|
||||
execution.StartedAt = time.Now()
|
||||
}
|
||||
return u.storage.CreateTestExecution(execution)
|
||||
}
|
||||
|
||||
// UpdateTestExecution updates an existing test execution
|
||||
func (u *TestResultUsecase) UpdateTestExecution(execution *happydns.TestExecution) error {
|
||||
return u.storage.UpdateTestExecution(execution)
|
||||
}
|
||||
|
||||
// CompleteTestExecution marks an execution as completed with a result
|
||||
func (u *TestResultUsecase) CompleteTestExecution(executionId happydns.Identifier, resultId happydns.Identifier) error {
|
||||
execution, err := u.storage.GetTestExecution(executionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
execution.Status = happydns.TestExecutionCompleted
|
||||
execution.CompletedAt = &now
|
||||
execution.ResultId = &resultId
|
||||
|
||||
return u.storage.UpdateTestExecution(execution)
|
||||
}
|
||||
|
||||
// FailTestExecution marks an execution as failed
|
||||
func (u *TestResultUsecase) FailTestExecution(executionId happydns.Identifier, errorMsg string) error {
|
||||
execution, err := u.storage.GetTestExecution(executionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
execution.Status = happydns.TestExecutionFailed
|
||||
execution.CompletedAt = &now
|
||||
|
||||
// Store error in a result
|
||||
result := &happydns.TestResult{
|
||||
PluginName: execution.PluginName,
|
||||
TestType: execution.TargetType,
|
||||
TargetId: execution.TargetId,
|
||||
OwnerId: execution.OwnerId,
|
||||
ExecutedAt: time.Now(),
|
||||
ScheduledTest: execution.ScheduleId != nil,
|
||||
Options: execution.Options,
|
||||
Status: happydns.PluginResultStatusKO,
|
||||
StatusLine: "Execution failed",
|
||||
Error: errorMsg,
|
||||
Duration: now.Sub(execution.StartedAt),
|
||||
}
|
||||
|
||||
if err := u.CreateTestResult(result); err != nil {
|
||||
return fmt.Errorf("failed to create error result: %w", err)
|
||||
}
|
||||
|
||||
execution.ResultId = &result.Id
|
||||
|
||||
return u.storage.UpdateTestExecution(execution)
|
||||
}
|
||||
|
||||
// DeleteCompletedExecutions removes execution records that are completed
|
||||
func (u *TestResultUsecase) DeleteCompletedExecutions(olderThan time.Duration) error {
|
||||
cutoffTime := time.Now().Add(-olderThan)
|
||||
|
||||
// Get active executions (this won't include completed ones)
|
||||
// We need a different query to get completed executions older than cutoff
|
||||
// For now, this is a placeholder
|
||||
|
||||
_ = cutoffTime
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -94,6 +94,12 @@ type Options struct {
|
|||
OIDCClients []OIDCSettings
|
||||
|
||||
PluginsDirectories []string
|
||||
|
||||
// MaxResultsPerTest is the maximum number of test results to keep per plugin+target combination
|
||||
MaxResultsPerTest int
|
||||
|
||||
// ResultRetentionDays is how long to keep test results before cleanup
|
||||
ResultRetentionDays int
|
||||
}
|
||||
|
||||
// GetBaseURL returns the full url to the absolute ExternalURL, including BaseURL.
|
||||
|
|
|
|||
|
|
@ -27,15 +27,17 @@ 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")
|
||||
ErrTestExecutionNotFound = errors.New("test execution not found")
|
||||
ErrTestResultNotFound = errors.New("test result 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")
|
||||
)
|
||||
|
||||
const TryAgainErr = "Sorry, we are currently unable to sent email validation link. Please try again later."
|
||||
|
|
|
|||
267
model/test_result.go
Normal file
267
model/test_result.go
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
// 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 happydns
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestScopeType represents the scope level at which a test is performed
|
||||
type TestScopeType int
|
||||
|
||||
const (
|
||||
TestScopeInstance TestScopeType = iota
|
||||
TestScopeUser
|
||||
TestScopeDomain
|
||||
TestScopeService
|
||||
TestScopeOnDemand
|
||||
)
|
||||
|
||||
// String returns a string representation of the test scope type
|
||||
func (t TestScopeType) String() string {
|
||||
switch t {
|
||||
case TestScopeInstance:
|
||||
return "instance"
|
||||
case TestScopeUser:
|
||||
return "user"
|
||||
case TestScopeDomain:
|
||||
return "domain"
|
||||
case TestScopeService:
|
||||
return "service"
|
||||
case TestScopeOnDemand:
|
||||
return "ondemand"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutionStatus represents the current state of a test execution
|
||||
type TestExecutionStatus int
|
||||
|
||||
const (
|
||||
TestExecutionPending TestExecutionStatus = iota
|
||||
TestExecutionRunning
|
||||
TestExecutionCompleted
|
||||
TestExecutionFailed
|
||||
)
|
||||
|
||||
// String returns a string representation of the test execution status
|
||||
func (t TestExecutionStatus) String() string {
|
||||
switch t {
|
||||
case TestExecutionPending:
|
||||
return "pending"
|
||||
case TestExecutionRunning:
|
||||
return "running"
|
||||
case TestExecutionCompleted:
|
||||
return "completed"
|
||||
case TestExecutionFailed:
|
||||
return "failed"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// TestResult stores the result of a test execution
|
||||
type TestResult struct {
|
||||
// Id is the unique identifier for this test result
|
||||
Id Identifier `json:"id" swaggertype:"string"`
|
||||
|
||||
// PluginName identifies which test plugin was executed
|
||||
PluginName string `json:"plugin_name"`
|
||||
|
||||
// TestType indicates the scope level of the test
|
||||
TestType TestScopeType `json:"test_type"`
|
||||
|
||||
// TargetId is the identifier of the target (User/Domain/Service)
|
||||
TargetId Identifier `json:"target_id" swaggertype:"string"`
|
||||
|
||||
// OwnerId is the owner of the test
|
||||
OwnerId Identifier `json:"owner_id" swaggertype:"string"`
|
||||
|
||||
// ExecutedAt is when the test was executed
|
||||
ExecutedAt time.Time `json:"executed_at"`
|
||||
|
||||
// ScheduledTest indicates if this was a scheduled (true) or on-demand (false) test
|
||||
ScheduledTest bool `json:"scheduled_test"`
|
||||
|
||||
// Options contains the merged plugin configuration used for this test
|
||||
Options PluginOptions `json:"options,omitempty"`
|
||||
|
||||
// Status is the overall test result status
|
||||
Status PluginResultStatus `json:"status"`
|
||||
|
||||
// StatusLine is a summary message of the test result
|
||||
StatusLine string `json:"status_line"`
|
||||
|
||||
// Report contains the full test report (plugin-specific structure)
|
||||
Report interface{} `json:"report,omitempty"`
|
||||
|
||||
// Duration is how long the test took to execute
|
||||
Duration time.Duration `json:"duration" swaggertype:"integer"`
|
||||
|
||||
// Error contains any error message if the execution failed
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TestSchedule defines a recurring test schedule
|
||||
type TestSchedule struct {
|
||||
// Id is the unique identifier for this schedule
|
||||
Id Identifier `json:"id" swaggertype:"string"`
|
||||
|
||||
// PluginName identifies which test plugin to execute
|
||||
PluginName string `json:"plugin_name"`
|
||||
|
||||
// OwnerId is the owner of the schedule
|
||||
OwnerId Identifier `json:"owner_id" swaggertype:"string"`
|
||||
|
||||
// TargetType indicates what type of target to test
|
||||
TargetType TestScopeType `json:"target_type"`
|
||||
|
||||
// TargetId is the identifier of the target to test
|
||||
TargetId Identifier `json:"target_id" swaggertype:"string"`
|
||||
|
||||
// Interval is how often to run the test
|
||||
Interval time.Duration `json:"interval" swaggertype:"integer"`
|
||||
|
||||
// Enabled indicates if the schedule is active
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// LastRun is when the test was last executed (nil if never run)
|
||||
LastRun *time.Time `json:"last_run,omitempty"`
|
||||
|
||||
// NextRun is when the test should next be executed
|
||||
NextRun time.Time `json:"next_run"`
|
||||
|
||||
// Options contains plugin-specific configuration
|
||||
Options PluginOptions `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// TestExecution tracks an in-progress or completed test execution
|
||||
type TestExecution struct {
|
||||
// Id is the unique identifier for this execution
|
||||
Id Identifier `json:"id" swaggertype:"string"`
|
||||
|
||||
// ScheduleId is the schedule that triggered this execution (nil for on-demand)
|
||||
ScheduleId *Identifier `json:"schedule_id,omitempty" swaggertype:"string"`
|
||||
|
||||
// PluginName identifies which test plugin is being executed
|
||||
PluginName string `json:"plugin_name"`
|
||||
|
||||
// OwnerId is the owner of the test
|
||||
OwnerId Identifier `json:"owner_id" swaggertype:"string"`
|
||||
|
||||
// TargetType indicates the scope level of the test
|
||||
TargetType TestScopeType `json:"target_type"`
|
||||
|
||||
// TargetId is the identifier of the target being tested
|
||||
TargetId Identifier `json:"target_id" swaggertype:"string"`
|
||||
|
||||
// Status is the current execution status
|
||||
Status TestExecutionStatus `json:"status"`
|
||||
|
||||
// StartedAt is when the execution began
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
|
||||
// CompletedAt is when the execution finished (nil if still running)
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
|
||||
// ResultId links to the TestResult (nil if execution not completed)
|
||||
ResultId *Identifier `json:"result_id,omitempty" swaggertype:"string"`
|
||||
|
||||
// Options contains the plugin configuration for this execution
|
||||
Options PluginOptions `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// TestResultUsecase defines business logic for test results
|
||||
type TestResultUsecase interface {
|
||||
// ListTestResultsByTarget retrieves test results for a specific target
|
||||
ListTestResultsByTarget(pluginName string, targetType TestScopeType, targetId Identifier, limit int) ([]*TestResult, error)
|
||||
|
||||
// ListAllTestResultsByTarget retrieves all test results for a target across all plugins
|
||||
ListAllTestResultsByTarget(targetType TestScopeType, targetId Identifier, userId Identifier, limit int) ([]*TestResult, error)
|
||||
|
||||
// GetTestResult retrieves a specific test result
|
||||
GetTestResult(pluginName string, targetType TestScopeType, targetId Identifier, resultId Identifier) (*TestResult, error)
|
||||
|
||||
// CreateTestResult stores a new test result and enforces retention policy
|
||||
CreateTestResult(result *TestResult) error
|
||||
|
||||
// DeleteTestResult removes a specific test result
|
||||
DeleteTestResult(pluginName string, targetType TestScopeType, targetId Identifier, resultId Identifier) error
|
||||
|
||||
// DeleteAllTestResults removes all results for a specific plugin+target combination
|
||||
DeleteAllTestResults(pluginName string, targetType TestScopeType, targetId Identifier) error
|
||||
|
||||
// GetTestExecution retrieves the status of a test execution
|
||||
GetTestExecution(executionId Identifier) (*TestExecution, error)
|
||||
|
||||
// CreateTestExecution creates a new test execution record
|
||||
CreateTestExecution(execution *TestExecution) error
|
||||
|
||||
// UpdateTestExecution updates an existing test execution
|
||||
UpdateTestExecution(execution *TestExecution) error
|
||||
|
||||
// CompleteTestExecution marks an execution as completed with a result
|
||||
CompleteTestExecution(executionId Identifier, resultId Identifier) error
|
||||
|
||||
// FailTestExecution marks an execution as failed
|
||||
FailTestExecution(executionId Identifier, errorMsg string) error
|
||||
}
|
||||
|
||||
// TestScheduleUsecase defines business logic for test schedules
|
||||
type TestScheduleUsecase interface {
|
||||
// ListUserSchedules retrieves all schedules for a specific user
|
||||
ListUserSchedules(userId Identifier) ([]*TestSchedule, error)
|
||||
|
||||
// ListSchedulesByTarget retrieves all schedules for a specific target
|
||||
ListSchedulesByTarget(targetType TestScopeType, targetId Identifier) ([]*TestSchedule, error)
|
||||
|
||||
// GetSchedule retrieves a specific schedule by ID
|
||||
GetSchedule(scheduleId Identifier) (*TestSchedule, error)
|
||||
|
||||
// CreateSchedule creates a new test schedule with validation
|
||||
CreateSchedule(schedule *TestSchedule) error
|
||||
|
||||
// UpdateSchedule updates an existing schedule
|
||||
UpdateSchedule(schedule *TestSchedule) error
|
||||
|
||||
// DeleteSchedule removes a schedule
|
||||
DeleteSchedule(scheduleId Identifier) error
|
||||
|
||||
// EnableSchedule enables a schedule
|
||||
EnableSchedule(scheduleId Identifier) error
|
||||
|
||||
// DisableSchedule disables a schedule
|
||||
DisableSchedule(scheduleId Identifier) error
|
||||
|
||||
// UpdateScheduleAfterRun updates a schedule after it has been executed
|
||||
UpdateScheduleAfterRun(scheduleId Identifier) error
|
||||
|
||||
// ListDueSchedules retrieves all enabled schedules that are due to run
|
||||
ListDueSchedules() ([]*TestSchedule, error)
|
||||
|
||||
// ValidateScheduleOwnership checks if a user owns a schedule
|
||||
ValidateScheduleOwnership(scheduleId Identifier, ownerId Identifier) error
|
||||
|
||||
// DeleteSchedulesForTarget removes all schedules for a target
|
||||
DeleteSchedulesForTarget(targetType TestScopeType, targetId Identifier) error
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ type UsecaseDependancies interface {
|
|||
ServiceSpecsUsecase() ServiceSpecsUsecase
|
||||
SessionUsecase() SessionUsecase
|
||||
TestPluginUsecase() TestPluginUsecase
|
||||
TestResultUsecase() TestResultUsecase
|
||||
UserUsecase() UserUsecase
|
||||
ZoneCorrectionApplierUsecase() ZoneCorrectionApplierUsecase
|
||||
ZoneImporterUsecase() ZoneImporterUsecase
|
||||
|
|
|
|||
|
|
@ -52,6 +52,17 @@ type UserSettings struct {
|
|||
|
||||
// ShowRRTypes tells if we show equivalent RRTypes in interface (for advanced users).
|
||||
ShowRRTypes bool `json:"showrrtypes,omitempty"`
|
||||
|
||||
// TestRetention overrides instance default for how long to keep test results (days)
|
||||
TestRetention int `json:"test_retention,omitempty"`
|
||||
|
||||
// DomainTestInterval is the default interval for domain-level tests (seconds)
|
||||
// Default: 86400 (24 hours)
|
||||
DomainTestInterval int64 `json:"domain_test_interval,omitempty"`
|
||||
|
||||
// ServiceTestInterval is the default interval for service-level tests (seconds)
|
||||
// Default: 3600 (1 hour)
|
||||
ServiceTestInterval int64 `json:"service_test_interval,omitempty"`
|
||||
}
|
||||
|
||||
func DefaultUserSettings() *UserSettings {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue