Some checks are pending
continuous-integration/drone/push Build is pending
272 lines
10 KiB
Go
272 lines
10 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 (
|
|
"context"
|
|
"io"
|
|
"log"
|
|
"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"
|
|
)
|
|
|
|
// CheckerController handles checker-related API endpoints.
|
|
type CheckerController struct {
|
|
engine happydns.CheckerEngine
|
|
OptionsUC *checkerUC.CheckerOptionsUsecase
|
|
planUC *checkerUC.CheckPlanUsecase
|
|
statusUC *checkerUC.CheckStatusUsecase
|
|
plannedProvider checkerUC.PlannedJobProvider
|
|
budgetChecker checkerUC.BudgetChecker
|
|
countManualTriggers bool
|
|
}
|
|
|
|
// NewCheckerController creates a new CheckerController.
|
|
//
|
|
// countManualTriggers controls whether manual (API-triggered) checker runs
|
|
// count against the user's MaxChecksPerDay budget. When true and
|
|
// budgetChecker is non-nil, TriggerCheck refuses the request with HTTP 429
|
|
// once the user is over budget and increments the counter on success.
|
|
// When false, manual triggers bypass the quota entirely (legacy behavior).
|
|
// The value is ignored when budgetChecker is nil.
|
|
func NewCheckerController(
|
|
engine happydns.CheckerEngine,
|
|
optionsUC *checkerUC.CheckerOptionsUsecase,
|
|
planUC *checkerUC.CheckPlanUsecase,
|
|
statusUC *checkerUC.CheckStatusUsecase,
|
|
plannedProvider checkerUC.PlannedJobProvider,
|
|
budgetChecker checkerUC.BudgetChecker,
|
|
countManualTriggers bool,
|
|
) *CheckerController {
|
|
return &CheckerController{
|
|
engine: engine,
|
|
OptionsUC: optionsUC,
|
|
planUC: planUC,
|
|
statusUC: statusUC,
|
|
plannedProvider: plannedProvider,
|
|
budgetChecker: budgetChecker,
|
|
countManualTriggers: countManualTriggers,
|
|
}
|
|
}
|
|
|
|
// StatusUC returns the CheckStatusUsecase for use by other controllers.
|
|
func (cc *CheckerController) StatusUC() *checkerUC.CheckStatusUsecase {
|
|
return cc.statusUC
|
|
}
|
|
|
|
// targetFromContext builds a CheckTarget from middleware context values.
|
|
func targetFromContext(c *gin.Context) happydns.CheckTarget {
|
|
user := middleware.MyUser(c)
|
|
target := happydns.CheckTarget{}
|
|
if user != nil {
|
|
target.UserId = user.Id.String()
|
|
}
|
|
if domain, exists := c.Get("domain"); exists {
|
|
d := domain.(*happydns.Domain)
|
|
target.DomainId = d.Id.String()
|
|
}
|
|
if sid, exists := c.Get("serviceid"); exists {
|
|
id := sid.(happydns.Identifier)
|
|
target.ServiceId = id.String()
|
|
if z, zExists := c.Get("zone"); zExists {
|
|
zone := z.(*happydns.Zone)
|
|
if _, svc := zone.FindService(id); svc != nil {
|
|
target.ServiceType = svc.Type
|
|
}
|
|
}
|
|
}
|
|
return target
|
|
}
|
|
|
|
// --- Global checker routes ---
|
|
|
|
// ListCheckers returns all registered checker definitions.
|
|
//
|
|
// @Summary List available checkers
|
|
// @Tags checkers
|
|
// @Produce json
|
|
// @Success 200 {object} map[string]checker.CheckerDefinition
|
|
// @Router /checkers [get]
|
|
func (cc *CheckerController) ListCheckers(c *gin.Context) {
|
|
c.JSON(http.StatusOK, checkerPkg.GetCheckers())
|
|
}
|
|
|
|
// GetChecker returns a specific checker definition.
|
|
//
|
|
// @Summary Get a checker definition
|
|
// @Tags checkers
|
|
// @Produce json
|
|
// @Param checkerId path string true "Checker ID"
|
|
// @Success 200 {object} checker.CheckerDefinition
|
|
// @Failure 404 {object} happydns.ErrorResponse
|
|
// @Router /checkers/{checkerId} [get]
|
|
func (cc *CheckerController) GetChecker(c *gin.Context) {
|
|
def, _ := c.Get("checker")
|
|
c.JSON(http.StatusOK, def)
|
|
}
|
|
|
|
// CheckerHandler is a middleware that validates the checkerId path parameter and sets "checker" in context.
|
|
func (cc *CheckerController) CheckerHandler(c *gin.Context) {
|
|
checkerID := c.Param("checkerId")
|
|
def := checkerPkg.FindChecker(checkerID)
|
|
if def == nil {
|
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Checker not found"})
|
|
return
|
|
}
|
|
c.Set("checker", def)
|
|
c.Next()
|
|
}
|
|
|
|
// --- Scoped routes (domain/service) ---
|
|
|
|
// ListAvailableChecks lists all checkers with their latest status for a target.
|
|
//
|
|
// @Summary List available checks with status
|
|
// @Tags checkers
|
|
// @Produce json
|
|
// @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.CheckerStatus
|
|
// @Router /domains/{domain}/checkers [get]
|
|
// @Router /domains/{domain}/zone/{zoneid}/{subdomain}/services/{serviceid}/checkers [get]
|
|
func (cc *CheckerController) ListAvailableChecks(c *gin.Context) {
|
|
target := targetFromContext(c)
|
|
|
|
result, err := cc.statusUC.ListCheckerStatuses(target)
|
|
if err != nil {
|
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
// TriggerCheck manually triggers a checker execution.
|
|
// By default the check runs asynchronously and returns an Execution (HTTP 202).
|
|
// Pass ?sync=true to block until the check completes and return a CheckEvaluation (HTTP 200).
|
|
//
|
|
// @Summary Trigger a manual check
|
|
// @Tags checkers
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param checkerId path string true "Checker ID"
|
|
// @Param sync query bool false "Run synchronously"
|
|
// @Param body body happydns.CheckerRunRequest false "Run request with options and enabled rules"
|
|
// @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
|
|
// @Success 202 {object} happydns.Execution
|
|
// @Failure 400 {object} happydns.ErrorResponse
|
|
// @Failure 429 {object} happydns.ErrorResponse
|
|
// @Router /domains/{domain}/checkers/{checkerId}/executions [post]
|
|
// @Router /domains/{domain}/zone/{zoneid}/{subdomain}/services/{serviceid}/checkers/{checkerId}/executions [post]
|
|
func (cc *CheckerController) TriggerCheck(c *gin.Context) {
|
|
cname := c.Param("checkerId")
|
|
|
|
var req happydns.CheckerRunRequest
|
|
// Body is optional; io.EOF means no body was sent, which is valid (no custom options or rules).
|
|
if err := c.ShouldBindJSON(&req); err != nil && err != io.EOF {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
target := targetFromContext(c)
|
|
if err := cc.OptionsUC.ValidateOptions(cname, happydns.TargetIdentifier(target.UserId), happydns.TargetIdentifier(target.DomainId), happydns.TargetIdentifier(target.ServiceId), req.Options, true); err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Enforce the daily check quota on manual triggers when configured.
|
|
// Interval=0 means "long-interval" in UserGater terms, so manual triggers
|
|
// are only denied at the hard limit — never by interval-aware throttling.
|
|
if cc.countManualTriggers && cc.budgetChecker != nil && !cc.budgetChecker.AllowWithInterval(target, 0) {
|
|
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"errmsg": "daily check quota exhausted; try again after 00:00 UTC"})
|
|
return
|
|
}
|
|
|
|
// Build a temporary plan from enabled rules if provided.
|
|
var plan *happydns.CheckPlan
|
|
if len(req.EnabledRules) > 0 {
|
|
plan = &happydns.CheckPlan{
|
|
CheckerID: cname,
|
|
Target: target,
|
|
Enabled: req.EnabledRules,
|
|
}
|
|
}
|
|
|
|
exec, err := cc.engine.CreateExecution(cname, target, plan)
|
|
if err != nil {
|
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
// Count the manual execution against the user's daily budget. Mirrors
|
|
// the scheduler's onExecute semantics: increment after CreateExecution
|
|
// succeeds, regardless of whether RunExecution later fails.
|
|
if cc.countManualTriggers && cc.budgetChecker != nil {
|
|
cc.budgetChecker.IncrementUsage(target)
|
|
}
|
|
|
|
if c.Query("sync") == "true" {
|
|
eval, err := cc.engine.RunExecution(c.Request.Context(), exec, plan, req.Options)
|
|
if err != nil {
|
|
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, eval)
|
|
} else {
|
|
go func() {
|
|
if _, err := cc.engine.RunExecution(context.WithoutCancel(c.Request.Context()), exec, plan, req.Options); err != nil {
|
|
log.Printf("async RunExecution error for checker %q execution %s: %v", cname, exec.Id.String(), err)
|
|
}
|
|
}()
|
|
c.JSON(http.StatusAccepted, exec)
|
|
}
|
|
}
|
|
|
|
// GetExecutionStatus returns the status of an execution.
|
|
//
|
|
// @Summary Get execution status
|
|
// @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.Execution
|
|
// @Failure 404 {object} happydns.ErrorResponse
|
|
// @Router /domains/{domain}/checkers/{checkerId}/executions/{executionId} [get]
|
|
// @Router /domains/{domain}/zone/{zoneid}/{subdomain}/services/{serviceid}/checkers/{checkerId}/executions/{executionId} [get]
|
|
func (cc *CheckerController) GetExecutionStatus(c *gin.Context) {
|
|
exec := c.MustGet("execution").(*happydns.Execution)
|
|
c.JSON(http.StatusOK, exec)
|
|
}
|