Some checks failed
continuous-integration/drone/push Build is failing
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.
307 lines
12 KiB
Go
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))
|
|
}
|