Build real cron engine + service-detail Cron UI

The cron backend was a stub: calculateNextRun returned now+1h for any
input and executeCronJob slept 2s then wrote "success" without running
the command. Now robfig/cron parses and validates schedules (400 on
invalid), a 60s scheduler goroutine executes due enabled jobs, and
commands run via docker exec sh -c inside the service's container with
stdout+stderr captured into cron_executions. Added Client.ExecRun for
attached output capture. UI: Cron tab on the service detail page with
create/edit/enable-toggle, manual Run, and execution history with logs.
Verified e2e against real Postgres + Docker (trigger and scheduler tick
both produce real captured output).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-17 17:01:16 +02:00
1 parent aab785afc1
commit a1e43524f5
9 files changed
+498 -26

No files matched your search

+32
View File
@@ -1,6 +1,7 @@
package docker
import (
"bytes"
"context"
"fmt"
"io"
@@ -15,6 +16,7 @@ import (
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
)
// Client wraps the Docker client with additional functionality
@@ -313,6 +315,36 @@ func (c *Client) ExecInspect(ctx context.Context, execID string) (container.Exec
return c.cli.ContainerExecInspect(ctx, execID)
}
// ExecRun runs a command in a container and returns its combined
// stdout/stderr output and exit code.
func (c *Client) ExecRun(ctx context.Context, containerID string, cmd []string) (string, int, error) {
exec, err := c.cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
Cmd: cmd,
AttachStdout: true,
AttachStderr: true,
})
if err != nil {
return "", -1, err
}
resp, err := c.cli.ContainerExecAttach(ctx, exec.ID, container.ExecAttachOptions{})
if err != nil {
return "", -1, err
}
defer resp.Close()
var buf bytes.Buffer
if _, err := stdcopy.StdCopy(&buf, &buf, resp.Reader); err != nil {
return "", -1, err
}
inspect, err := c.cli.ContainerExecInspect(ctx, exec.ID)
if err != nil {
return buf.String(), -1, err
}
return buf.String(), inspect.ExitCode, nil
}
// GetImageInfo returns information about a Docker image
func (c *Client) GetImageInfo(ctx context.Context, imageName string) (*ImageInfo, error) {
images, err := c.cli.ImageList(ctx, image.ListOptions{})