happyDomain/internal/api/controller/checkresult_controller.go
Pierre-Olivier Mercier 4cccbc6661 Implement auto-fill variables for checker option fields
Add an AutoFill attribute to the Field struct that marks option fields
as automatically resolved by the software based on test context, rather
than requiring user input. Auto-fill always overrides any user-provided
value at execution time.
2026-02-19 20:02:18 +07:00

525 lines
17 KiB
Go

// 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"
"net/http"
"github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/internal/api/middleware"
"git.happydns.org/happyDomain/model"
)
// CheckResultController handles check result operations
type CheckResultController struct {
scope happydns.CheckScopeType
checkerUC happydns.CheckerUsecase
checkResultUC happydns.CheckResultUsecase
checkerScheduleUC happydns.CheckerScheduleUsecase
checkScheduler happydns.SchedulerUsecase
}
func NewCheckResultController(
scope happydns.CheckScopeType,
checkerUC happydns.CheckerUsecase,
checkResultUC happydns.CheckResultUsecase,
checkerScheduleUC happydns.CheckerScheduleUsecase,
checkScheduler happydns.SchedulerUsecase,
) *CheckResultController {
return &CheckResultController{
scope: scope,
checkerUC: checkerUC,
checkResultUC: checkResultUC,
checkerScheduleUC: checkerScheduleUC,
checkScheduler: checkScheduler,
}
}
// getTargetFromContext extracts the target ID from context based on scope
func (tc *CheckResultController) getTargetFromContext(c *gin.Context) (happydns.Identifier, error) {
switch tc.scope {
case happydns.CheckScopeUser:
user := c.MustGet("user").(*happydns.User)
return user.Id, nil
case happydns.CheckScopeDomain:
domain := c.MustGet("domain").(*happydns.Domain)
return domain.Id, nil
case happydns.CheckScopeService:
// Services are stored by ID in context
serviceID := c.MustGet("serviceid").(happydns.Identifier)
return serviceID, nil
default:
return happydns.Identifier{}, fmt.Errorf("unsupported scope")
}
}
// ListAvailableChecks lists all available check plugins for the target scope
//
// @Summary List available checks
// @Description Retrieves all available check plugins for the target scope with their last execution status if enabled
// @Tags checks
// @Produce json
// @Param domain path string true "Domain identifier"
// @Success 200 {array} object "List of available checks"
// @Failure 500 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checks [get]
func (tc *CheckResultController) ListAvailableChecks(c *gin.Context) {
targetID, err := tc.getTargetFromContext(c)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
// Get all check plugins
plugins, err := tc.checkerUC.ListCheckers()
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
// Get schedules for this target
schedules, err := tc.checkerScheduleUC.ListSchedulesByTarget(tc.scope, targetID)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
// Build schedule map
scheduleMap := make(map[string]*happydns.CheckerSchedule)
for _, sched := range schedules {
scheduleMap[sched.CheckerName] = sched
}
// Build response with last results
var checks []happydns.CheckerStatus
for checkername, check := range *plugins {
// Filter plugins by scope
if tc.scope == happydns.CheckScopeDomain && !check.Availability().ApplyToDomain {
continue
}
if tc.scope == happydns.CheckScopeService && !check.Availability().ApplyToService {
continue
}
info := happydns.CheckerStatus{
CheckerName: checkername,
Enabled: true, // enabled by default unless explicitly disabled via a schedule
}
// Check if there's a schedule
if sched, ok := scheduleMap[checkername]; ok {
info.Enabled = sched.Enabled
info.Schedule = sched
// Get last result
results, err := tc.checkResultUC.ListCheckResultsByTarget(checkername, tc.scope, targetID, 1)
if err == nil && len(results) > 0 {
info.LastResult = results[0]
}
}
checks = append(checks, info)
}
c.JSON(http.StatusOK, checks)
}
// ListLatestCheckResults retrieves the lacheck check results for a specific plugin
//
// @Summary Get lacheck check results
// @Description Retrieves the 5 most recent check results for a specific plugin and target
// @Tags checks
// @Produce json
// @Param domain path string true "Domain identifier"
// @Param cname path string true "Check plugin name"
// @Success 200 {array} happydns.CheckResult
// @Failure 500 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checks/{cname} [get]
func (tc *CheckResultController) ListLatestCheckResults(c *gin.Context) {
checkName := c.Param("cname")
targetID, err := tc.getTargetFromContext(c)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
results, err := tc.checkResultUC.ListCheckResultsByTarget(checkName, tc.scope, targetID, 5)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, results)
}
// TriggerCheck triggers an on-demand check execution
//
// @Summary Trigger check execution
// @Description Triggers an immediate check execution and returns the execution ID
// @Tags checks
// @Accept json
// @Produce json
// @Param domain path string true "Domain identifier"
// @Param cname path string true "Check 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}/checks/{cname} [post]
func (tc *CheckResultController) TriggerCheck(c *gin.Context) {
user := middleware.MyUser(c)
checkName := c.Param("cname")
targetID, err := tc.getTargetFromContext(c)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
// Parse run options
var options happydns.SetCheckerOptionsRequest
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.CheckScopeDomain:
domainID = &targetID
case happydns.CheckScopeService:
serviceID = &targetID
}
mergedOptions, err := tc.checkerUC.BuildMergedCheckerOptions(checkName, &user.Id, domainID, serviceID, options.Options)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
// Trigger the test via scheduler (returns error if scheduler is disabled)
executionID, err := tc.checkScheduler.TriggerOnDemandCheck(checkName, 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()})
}
// GetCheckerOptions retrieves plugin options for the target scope
//
// @Summary Get check plugin options
// @Description Retrieves configuration options for a checker at the target scope
// @Tags checks
// @Produce json
// @Param domain path string true "Domain identifier"
// @Param cname path string true "Check plugin name"
// @Success 200 {object} happydns.CheckerOptions
// @Failure 500 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checks/{cname}/options [get]
func (tc *CheckResultController) GetCheckerOptions(c *gin.Context) {
user := middleware.MyUser(c)
checkName := c.Param("cname")
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.CheckScopeDomain:
domainID = &targetID
case happydns.CheckScopeService:
serviceID = &targetID
}
opts, err := tc.checkerUC.GetStoredCheckerOptionsNoDefault(checkName, &user.Id, domainID, serviceID)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, opts)
}
// AddCheckerOptions adds or overwrites specific options
//
// @Summary Add check plugin options
// @Description Adds or overwrites specific options for a check plugin at the target scope
// @Tags checks
// @Accept json
// @Produce json
// @Param domain path string true "Domain identifier"
// @Param cname path string true "Check plugin name"
// @Param body body happydns.CheckerOptions true "Options to add"
// @Success 200 {object} bool
// @Failure 400 {object} happydns.ErrorResponse
// @Failure 500 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checks/{cname}/options [post]
func (tc *CheckResultController) AddCheckerOptions(c *gin.Context) {
user := middleware.MyUser(c)
checkName := c.Param("cname")
targetID, err := tc.getTargetFromContext(c)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
var options happydns.CheckerOptions
if err = c.ShouldBindJSON(&options); err != nil {
middleware.ErrorResponse(c, http.StatusBadRequest, err)
return
}
var domainID, serviceID *happydns.Identifier
switch tc.scope {
case happydns.CheckScopeDomain:
domainID = &targetID
case happydns.CheckScopeService:
serviceID = &targetID
}
err = tc.checkerUC.OverwriteSomeCheckerOptions(checkName, &user.Id, domainID, serviceID, options)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, true)
}
// ChangeCheckerOptions replaces all options
//
// @Summary Replace check plugin options
// @Description Replaces all options for a check plugin at the target scope
// @Tags checks
// @Accept json
// @Produce json
// @Param domain path string true "Domain identifier"
// @Param cname path string true "Check plugin name"
// @Param body body happydns.CheckerOptions true "New complete options"
// @Success 200 {object} bool
// @Failure 400 {object} happydns.ErrorResponse
// @Failure 500 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checks/{cname}/options [put]
func (tc *CheckResultController) ChangeCheckerOptions(c *gin.Context) {
user := middleware.MyUser(c)
checkName := c.Param("cname")
targetID, err := tc.getTargetFromContext(c)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
var options happydns.CheckerOptions
if err = c.ShouldBindJSON(&options); err != nil {
middleware.ErrorResponse(c, http.StatusBadRequest, err)
return
}
var domainID, serviceID *happydns.Identifier
switch tc.scope {
case happydns.CheckScopeDomain:
domainID = &targetID
case happydns.CheckScopeService:
serviceID = &targetID
}
err = tc.checkerUC.SetCheckerOptions(checkName, &user.Id, domainID, serviceID, options)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, true)
}
// GetCheckExecutionStatus retrieves the status of a check execution
//
// @Summary Get check execution status
// @Description Retrieves the current status of a check execution
// @Tags checks
// @Produce json
// @Param domain path string true "Domain identifier"
// @Param cname path string true "Check plugin name"
// @Param execution_id path string true "Execution ID"
// @Success 200 {object} happydns.CheckExecution
// @Failure 404 {object} happydns.ErrorResponse
// @Failure 500 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checks/{cname}/executions/{execution_id} [get]
func (tc *CheckResultController) GetCheckExecutionStatus(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.checkResultUC.GetCheckExecution(executionID)
if err != nil {
middleware.ErrorResponse(c, http.StatusNotFound, err)
return
}
c.JSON(http.StatusOK, execution)
}
// ListCheckResults lists all results for a check plugin
//
// @Summary List check results
// @Description Lists all check results for a specific check plugin and target
// @Tags checks
// @Produce json
// @Param domain path string true "Domain identifier"
// @Param cname path string true "Check plugin name"
// @Param limit query int false "Maximum number of results to return (default: 10)"
// @Success 200 {array} happydns.CheckResult
// @Failure 500 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checks/{cname}/results [get]
func (tc *CheckResultController) ListCheckResults(c *gin.Context) {
checkName := c.Param("cname")
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.checkResultUC.ListCheckResultsByTarget(checkName, tc.scope, targetID, limit)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, results)
}
// DropCheckResults deletes all results for a check plugin
//
// @Summary Delete all check results
// @Description Deletes all check results for a specific check plugin and target
// @Tags checks
// @Produce json
// @Param domain path string true "Domain identifier"
// @Param cname path string true "Check plugin name"
// @Success 204 "No Content"
// @Failure 500 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checks/{cname}/results [delete]
func (tc *CheckResultController) DropCheckResults(c *gin.Context) {
checkName := c.Param("cname")
targetID, err := tc.getTargetFromContext(c)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
err = tc.checkResultUC.DeleteAllCheckResults(checkName, tc.scope, targetID)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
c.Status(http.StatusNoContent)
}
// GetCheckPluginResult retrieves a specific check result
//
// @Summary Get check result
// @Description Retrieves a specific check result by ID
// @Tags checks
// @Produce json
// @Param domain path string true "Domain identifier"
// @Param cname path string true "Check plugin name"
// @Param result_id path string true "Result ID"
// @Success 200 {object} happydns.CheckResult
// @Failure 404 {object} happydns.ErrorResponse
// @Failure 500 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checks/{cname}/results/{result_id} [get]
func (tc *CheckResultController) GetCheckResult(c *gin.Context) {
checkName := c.Param("cname")
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.checkResultUC.GetCheckResult(checkName, tc.scope, targetID, resultID)
if err != nil {
middleware.ErrorResponse(c, http.StatusNotFound, err)
return
}
c.JSON(http.StatusOK, result)
}
// DropCheckResult deletes a specific check result
//
// @Summary Delete check result
// @Description Deletes a specific check result by ID
// @Tags checks
// @Produce json
// @Param domain path string true "Domain identifier"
// @Param cname path string true "Check 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}/checks/{cname}/results/{result_id} [delete]
func (tc *CheckResultController) DropCheckResult(c *gin.Context) {
checkName := c.Param("cname")
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.checkResultUC.DeleteCheckResult(checkName, tc.scope, targetID, resultID)
if err != nil {
middleware.ErrorResponse(c, http.StatusNotFound, err)
return
}
c.Status(http.StatusNoContent)
}