feat: fire scheduled workflow runs from the housekeeping leader

This commit is contained in:
2026-08-04 13:53:42 +01:00
parent d0e1cc4ad6
commit a1e6986a64
3 changed files with 213 additions and 0 deletions
+52
View File
@@ -13,7 +13,9 @@ import (
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/auth"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/services"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/workflowsched"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/v2/mongo"
)
func registerWorkflowRoutes(g *gin.RouterGroup) {
@@ -34,6 +36,8 @@ func registerWorkflowRoutes(g *gin.RouterGroup) {
g.DELETE("/workflows/:id", deleteWorkflow)
g.POST("/workflows/:id/run", runWorkflow)
g.GET("/workflows/:id/runs", listWorkflowRuns)
g.PUT("/workflows/:id/schedule", putWorkflowSchedule)
g.GET("/workflows/:id/schedule/preview", previewWorkflowSchedule)
g.GET("/runs/:runId", getRun)
g.POST("/runs/:runId/cancel", cancelRun)
@@ -386,3 +390,51 @@ func cancelRun(c *gin.Context) {
services.LogEvent(auth.InstanceID(c), "workflow.run_cancelled", actorFromCtx(c), "", c.Param("runId"), "run cancelled")
c.JSON(http.StatusOK, gin.H{"cancelled": true})
}
func putWorkflowSchedule(c *gin.Context) {
var body models.Schedule
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
instanceID := auth.InstanceID(c)
next, err := services.SetSchedule(instanceID, c.Param("id"), &body)
if err != nil {
if errors.Is(err, workflowsched.ErrBadSchedule) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if errors.Is(err, mongo.ErrNoDocuments) {
c.JSON(http.StatusNotFound, gin.H{"error": "workflow not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
services.LogEvent(instanceID, "workflow.schedule_updated", actorFromCtx(c), "", c.Param("id"),
fmt.Sprintf("schedule %q %s enabled=%v", body.Cron, body.TZ, body.Enabled))
c.JSON(http.StatusOK, gin.H{"schedule": body, "next_run_at": next})
}
// previewWorkflowSchedule exists so the browser and the scheduler agree on
// what a cron string means. A client-side cron parser that disagrees with the
// server by one field is a bug found in production, at night.
func previewWorkflowSchedule(c *gin.Context) {
expr := c.Query("cron")
tz := c.Query("tz")
occurrences := make([]time.Time, 0, 3)
from := time.Now()
for i := 0; i < 3; i++ {
next, err := workflowsched.NextOccurrence(expr, tz, from)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
occurrences = append(occurrences, next)
from = next
}
c.JSON(http.StatusOK, gin.H{"occurrences": occurrences})
}