happyDomain/internal/api/controller/checker_results.go
Pierre-Olivier Mercier e0dc19614f
Some checks failed
continuous-integration/drone/push Build is failing
checker: enforce MaxChecksPerDay quota with interval-aware throttling
Wire UserQuota.MaxChecksPerDay field into the scheduler via the
UserGater: an in-memory daily counter per user
(reset at UTC midnight): gates scheduled executions, with a two-tier
heuristic that skips short-interval jobs first once the budget is 80%
consumed so rare/important checks are not starved by frequent
pings. Planned executions returned by ListPlannedExecutions are marked
with a new ExecutionRateLimited status when the user is over
budget. Manual API triggers bypass the quota.
2026-04-16 15:18:43 +07:00

307 lines
12 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"
checkerPkg "git.happydns.org/happyDomain/internal/checker"
checkerUC "git.happydns.org/happyDomain/internal/usecase/checker"
"git.happydns.org/happyDomain/model"
)
// ExecutionHandler is a middleware that validates the executionId path parameter,
// checks target scope, and sets "execution" in context.
func (cc *CheckerController) ExecutionHandler(c *gin.Context) {
execID, err := happydns.NewIdentifierFromString(c.Param("executionId"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid execution ID"})
return
}
exec, err := cc.statusUC.GetExecution(targetFromContext(c), execID)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Execution not found"})
return
}
c.Set("execution", exec)
c.Next()
}
// ListExecutions returns executions for a checker on a target.
//
// @Summary List executions for a checker
// @Tags checkers
// @Produce json
// @Param checkerId path string true "Checker ID"
// @Param limit query int false "Maximum number of results"
// @Param include_planned query bool false "Include upcoming planned executions from the scheduler"
// @Param domain path string true "Domain identifier"
// @Param zoneid path string true "Zone identifier"
// @Param subdomain path string true "Subdomain"
// @Param serviceid path string true "Service identifier"
// @Success 200 {array} happydns.Execution
// @Router /domains/{domain}/checkers/{checkerId}/executions [get]
// @Router /domains/{domain}/zone/{zoneid}/{subdomain}/services/{serviceid}/checkers/{checkerId}/executions [get]
func (cc *CheckerController) ListExecutions(c *gin.Context) {
cname := c.Param("checkerId")
target := targetFromContext(c)
limit := getLimitParam(c, 0)
execs, err := cc.statusUC.ListExecutionsByChecker(cname, target, limit)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
if execs == nil {
execs = []*happydns.Execution{}
}
if c.Query("include_planned") == "true" || c.Query("include_planned") == "1" {
planned := checkerUC.ListPlannedExecutions(cc.plannedProvider, cc.budgetChecker, cname, target)
execs = append(planned, execs...)
}
c.JSON(http.StatusOK, execs)
}
// DeleteExecution deletes an execution record.
//
// @Summary Delete an execution
// @Tags checkers
// @Produce json
// @Param checkerId path string true "Checker ID"
// @Param executionId path string true "Execution ID"
// @Param domain path string true "Domain identifier"
// @Param zoneid path string true "Zone identifier"
// @Param subdomain path string true "Subdomain"
// @Param serviceid path string true "Service identifier"
// @Success 204
// @Failure 400 {object} happydns.ErrorResponse
// @Failure 404 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checkers/{checkerId}/executions/{executionId} [delete]
// @Router /domains/{domain}/zone/{zoneid}/{subdomain}/services/{serviceid}/checkers/{checkerId}/executions/{executionId} [delete]
func (cc *CheckerController) DeleteExecution(c *gin.Context) {
exec := c.MustGet("execution").(*happydns.Execution)
if err := cc.statusUC.DeleteExecution(targetFromContext(c), exec.Id); err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
c.Status(http.StatusNoContent)
}
// DeleteCheckerExecutions deletes all executions for a checker on a target.
//
// @Summary Delete all executions for a checker
// @Tags checkers
// @Produce json
// @Param checkerId path string true "Checker ID"
// @Param domain path string true "Domain identifier"
// @Param zoneid path string true "Zone identifier"
// @Param subdomain path string true "Subdomain"
// @Param serviceid path string true "Service identifier"
// @Success 204
// @Failure 400 {object} happydns.ErrorResponse
// @Failure 404 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checkers/{checkerId}/executions [delete]
// @Router /domains/{domain}/zone/{zoneid}/{subdomain}/services/{serviceid}/checkers/{checkerId}/executions [delete]
func (cc *CheckerController) DeleteCheckerExecutions(c *gin.Context) {
cname := c.Param("checkerId")
target := targetFromContext(c)
if err := cc.statusUC.DeleteExecutionsByChecker(cname, target); err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
c.Status(http.StatusNoContent)
}
// GetExecutionObservations returns the observation snapshot for an execution.
//
// @Summary Get observations for an execution
// @Tags checkers
// @Produce json
// @Param checkerId path string true "Checker ID"
// @Param executionId path string true "Execution ID"
// @Param domain path string true "Domain identifier"
// @Param zoneid path string true "Zone identifier"
// @Param subdomain path string true "Subdomain"
// @Param serviceid path string true "Service identifier"
// @Success 200 {object} happydns.ObservationSnapshot
// @Failure 404 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checkers/{checkerId}/executions/{executionId}/observations [get]
// @Router /domains/{domain}/zone/{zoneid}/{subdomain}/services/{serviceid}/checkers/{checkerId}/executions/{executionId}/observations [get]
func (cc *CheckerController) GetExecutionObservations(c *gin.Context) {
exec := c.MustGet("execution").(*happydns.Execution)
snap, err := cc.statusUC.GetObservationsByExecution(targetFromContext(c), exec.Id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Observations not available"})
return
}
c.JSON(http.StatusOK, snap)
}
// GetExecutionObservation returns a specific observation key from an execution's snapshot.
//
// @Summary Get a specific observation for an execution
// @Tags checkers
// @Produce json
// @Param checkerId path string true "Checker ID"
// @Param executionId path string true "Execution ID"
// @Param obsKey path string true "Observation key"
// @Param domain path string true "Domain identifier"
// @Param zoneid path string true "Zone identifier"
// @Param subdomain path string true "Subdomain"
// @Param serviceid path string true "Service identifier"
// @Success 200 {object} any
// @Failure 404 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checkers/{checkerId}/executions/{executionId}/observations/{obsKey} [get]
// @Router /domains/{domain}/zone/{zoneid}/{subdomain}/services/{serviceid}/checkers/{checkerId}/executions/{executionId}/observations/{obsKey} [get]
func (cc *CheckerController) GetExecutionObservation(c *gin.Context) {
exec := c.MustGet("execution").(*happydns.Execution)
obsKey := c.Param("obsKey")
val, err := cc.statusUC.GetSnapshotByExecution(targetFromContext(c), exec.Id, obsKey)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Observation not available"})
return
}
c.Data(http.StatusOK, "application/json; charset=utf-8", val)
}
// GetExecutionResults returns the evaluation (per-rule states) for an execution.
//
// @Summary Get results for an execution
// @Tags checkers
// @Produce json
// @Param checkerId path string true "Checker ID"
// @Param executionId path string true "Execution ID"
// @Param domain path string true "Domain identifier"
// @Param zoneid path string true "Zone identifier"
// @Param subdomain path string true "Subdomain"
// @Param serviceid path string true "Service identifier"
// @Success 200 {object} happydns.CheckEvaluation
// @Failure 404 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checkers/{checkerId}/executions/{executionId}/results [get]
// @Router /domains/{domain}/zone/{zoneid}/{subdomain}/services/{serviceid}/checkers/{checkerId}/executions/{executionId}/results [get]
func (cc *CheckerController) GetExecutionResults(c *gin.Context) {
exec := c.MustGet("execution").(*happydns.Execution)
eval, err := cc.statusUC.GetResultsByExecution(targetFromContext(c), exec.Id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Results not available"})
return
}
c.JSON(http.StatusOK, eval)
}
// GetExecutionResult returns a specific rule's result from an execution.
//
// @Summary Get a specific rule result for an execution
// @Tags checkers
// @Produce json
// @Param checkerId path string true "Checker ID"
// @Param executionId path string true "Execution ID"
// @Param ruleName path string true "Rule name"
// @Param domain path string true "Domain identifier"
// @Param zoneid path string true "Zone identifier"
// @Param subdomain path string true "Subdomain"
// @Param serviceid path string true "Service identifier"
// @Success 200 {object} checker.CheckState
// @Failure 404 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checkers/{checkerId}/executions/{executionId}/results/{ruleName} [get]
// @Router /domains/{domain}/zone/{zoneid}/{subdomain}/services/{serviceid}/checkers/{checkerId}/executions/{executionId}/results/{ruleName} [get]
func (cc *CheckerController) GetExecutionResult(c *gin.Context) {
exec := c.MustGet("execution").(*happydns.Execution)
eval, err := cc.statusUC.GetResultsByExecution(targetFromContext(c), exec.Id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Results not available"})
return
}
ruleName := c.Param("ruleName")
for _, state := range eval.States {
if state.Code == ruleName {
c.JSON(http.StatusOK, state)
return
}
}
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Rule result not found"})
}
// GetExecutionHTMLReport returns the HTML report for a specific observation of an execution.
//
// @Summary Get execution observation HTML report
// @Description Returns the full HTML document generated from an observation's data. Only available for observation providers that implement HTML reporting.
// @Tags checkers
// @Produce html
// @Param checkerId path string true "Checker ID"
// @Param executionId path string true "Execution ID"
// @Param obsKey path string true "Observation key"
// @Param domain path string true "Domain identifier"
// @Param zoneid path string true "Zone identifier"
// @Param subdomain path string true "Subdomain"
// @Param serviceid path string true "Service identifier"
// @Success 200 {string} string "HTML document"
// @Failure 404 {object} happydns.ErrorResponse
// @Failure 500 {object} happydns.ErrorResponse
// @Router /domains/{domain}/checkers/{checkerId}/executions/{executionId}/observations/{obsKey}/report [get]
// @Router /domains/{domain}/zone/{zoneid}/{subdomain}/services/{serviceid}/checkers/{checkerId}/executions/{executionId}/observations/{obsKey}/report [get]
func (cc *CheckerController) GetExecutionHTMLReport(c *gin.Context) {
exec := c.MustGet("execution").(*happydns.Execution)
obsKey := c.Param("obsKey")
val, err := cc.statusUC.GetSnapshotByExecution(targetFromContext(c), exec.Id, obsKey)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Observation not available"})
return
}
htmlContent, supported, err := checkerPkg.GetHTMLReport(obsKey, val)
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
return
}
if !supported {
middleware.ErrorResponse(c, http.StatusNotFound, fmt.Errorf("observation %q does not support HTML reports", obsKey))
return
}
c.Header("Content-Security-Policy", "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'self'")
c.Header("X-Content-Type-Options", "nosniff")
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(htmlContent))
}