This commit is contained in:
2026-03-26 13:02:43 +00:00
parent a689f8b9da
commit eb7b519e49
64 changed files with 7081 additions and 5572 deletions

View File

@@ -0,0 +1,87 @@
package jobs
import (
"context"
"strings"
appv1 "stream.api/internal/gen/proto/app/v1"
)
type Handler struct {
module *Module
}
func NewHandler(module *Module) *Handler { return &Handler{module: module} }
func (h *Handler) ListAdminJobs(ctx context.Context, req *appv1.ListAdminJobsRequest) (*appv1.ListAdminJobsResponse, error) {
useCursorPagination := req.Cursor != nil || int(req.GetPageSize()) > 0
result, err := h.module.ListAdminJobs(ctx, ListAdminJobsQuery{AgentID: strings.TrimSpace(req.GetAgentId()), Offset: int(req.GetOffset()), Limit: int(req.GetLimit()), Cursor: req.Cursor, PageSize: int(req.GetPageSize()), UseCursorPagination: useCursorPagination})
if err != nil {
return nil, err
}
return presentListAdminJobsResponse(result), nil
}
func (h *Handler) GetAdminJob(ctx context.Context, req *appv1.GetAdminJobRequest) (*appv1.GetAdminJobResponse, error) {
job, err := h.module.GetAdminJob(ctx, GetAdminJobQuery{ID: strings.TrimSpace(req.GetId())})
if err != nil {
return nil, err
}
return presentGetAdminJobResponse(job), nil
}
func (h *Handler) GetAdminJobLogs(ctx context.Context, req *appv1.GetAdminJobLogsRequest) (*appv1.GetAdminJobLogsResponse, error) {
response, err := h.GetAdminJob(ctx, &appv1.GetAdminJobRequest{Id: req.GetId()})
if err != nil {
return nil, err
}
return &appv1.GetAdminJobLogsResponse{Logs: response.GetJob().GetLogs()}, nil
}
func (h *Handler) CreateAdminJob(ctx context.Context, req *appv1.CreateAdminJobRequest) (*appv1.CreateAdminJobResponse, error) {
job, err := h.module.CreateAdminJob(ctx, CreateAdminJobCommand{Command: strings.TrimSpace(req.GetCommand()), Image: strings.TrimSpace(req.GetImage()), Name: strings.TrimSpace(req.GetName()), UserID: strings.TrimSpace(req.GetUserId()), VideoID: req.VideoId, Env: req.GetEnv(), Priority: int(req.GetPriority()), TimeLimit: req.GetTimeLimit()})
if err != nil {
return nil, err
}
return presentCreateAdminJobResponse(job), nil
}
func (h *Handler) CancelAdminJob(ctx context.Context, req *appv1.CancelAdminJobRequest) (*appv1.CancelAdminJobResponse, error) {
result, err := h.module.CancelAdminJob(ctx, CancelAdminJobCommand{ID: strings.TrimSpace(req.GetId())})
if err != nil {
return nil, err
}
return presentCancelAdminJobResponse(result), nil
}
func (h *Handler) RetryAdminJob(ctx context.Context, req *appv1.RetryAdminJobRequest) (*appv1.RetryAdminJobResponse, error) {
job, err := h.module.RetryAdminJob(ctx, RetryAdminJobCommand{ID: strings.TrimSpace(req.GetId())})
if err != nil {
return nil, err
}
return presentRetryAdminJobResponse(job), nil
}
func (h *Handler) ListAdminAgents(ctx context.Context, _ *appv1.ListAdminAgentsRequest) (*appv1.ListAdminAgentsResponse, error) {
items, err := h.module.ListAdminAgents(ctx)
if err != nil {
return nil, err
}
return presentListAdminAgentsResponse(items), nil
}
func (h *Handler) RestartAdminAgent(ctx context.Context, req *appv1.RestartAdminAgentRequest) (*appv1.AdminAgentCommandResponse, error) {
statusValue, err := h.module.RestartAdminAgent(ctx, AgentCommand{ID: strings.TrimSpace(req.GetId()), Command: "restart", Success: "restart command sent"})
if err != nil {
return nil, err
}
return presentAgentCommandResponse(statusValue), nil
}
func (h *Handler) UpdateAdminAgent(ctx context.Context, req *appv1.UpdateAdminAgentRequest) (*appv1.AdminAgentCommandResponse, error) {
statusValue, err := h.module.UpdateAdminAgent(ctx, AgentCommand{ID: strings.TrimSpace(req.GetId()), Command: "update", Success: "update command sent"})
if err != nil {
return nil, err
}
return presentAgentCommandResponse(statusValue), nil
}

View File

@@ -0,0 +1,184 @@
package jobs
import (
"context"
"encoding/json"
"errors"
"strings"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gorm.io/gorm"
"stream.api/internal/modules/common"
videodomain "stream.api/internal/video"
)
type Module struct {
runtime *common.Runtime
}
func New(runtime *common.Runtime) *Module {
return &Module{runtime: runtime}
}
func (m *Module) ListAdminJobs(ctx context.Context, queryValue ListAdminJobsQuery) (*ListAdminJobsResult, error) {
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
return nil, err
}
videoService := m.runtime.VideoService()
if videoService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
var (
result *videodomain.PaginatedJobs
err error
)
cursor := ""
if queryValue.Cursor != nil {
cursor = *queryValue.Cursor
}
if queryValue.UseCursorPagination {
result, err = videoService.ListJobsByCursor(ctx, queryValue.AgentID, cursor, queryValue.PageSize)
} else if queryValue.AgentID != "" {
result, err = videoService.ListJobsByAgent(ctx, queryValue.AgentID, queryValue.Offset, queryValue.Limit)
} else {
result, err = videoService.ListJobs(ctx, queryValue.Offset, queryValue.Limit)
}
if err != nil {
if errors.Is(err, videodomain.ErrInvalidJobCursor) {
return nil, status.Error(codes.InvalidArgument, "Invalid job cursor")
}
return nil, status.Error(codes.Internal, "Failed to list jobs")
}
var nextCursor *string
if strings.TrimSpace(result.NextCursor) != "" {
value := result.NextCursor
nextCursor = &value
}
return &ListAdminJobsResult{Jobs: result.Jobs, Total: result.Total, Offset: result.Offset, Limit: result.Limit, HasMore: result.HasMore, PageSize: result.PageSize, NextCursor: nextCursor}, nil
}
func (m *Module) GetAdminJob(ctx context.Context, queryValue GetAdminJobQuery) (*videodomain.Job, error) {
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
return nil, err
}
videoService := m.runtime.VideoService()
if videoService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
if queryValue.ID == "" {
return nil, status.Error(codes.NotFound, "Job not found")
}
job, err := videoService.GetJob(ctx, queryValue.ID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Job not found")
}
return nil, status.Error(codes.Internal, "Failed to load job")
}
return job, nil
}
func (m *Module) CreateAdminJob(ctx context.Context, cmd CreateAdminJobCommand) (*videodomain.Job, error) {
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
return nil, err
}
videoService := m.runtime.VideoService()
if videoService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
if cmd.Command == "" {
return nil, status.Error(codes.InvalidArgument, "Command is required")
}
image := strings.TrimSpace(cmd.Image)
if image == "" {
image = "alpine"
}
name := strings.TrimSpace(cmd.Name)
if name == "" {
name = cmd.Command
}
payload, err := json.Marshal(map[string]any{"image": image, "commands": []string{cmd.Command}, "environment": cmd.Env})
if err != nil {
return nil, status.Error(codes.Internal, "Failed to create job payload")
}
videoID := ""
if cmd.VideoID != nil {
videoID = strings.TrimSpace(*cmd.VideoID)
}
job, err := videoService.CreateJob(ctx, strings.TrimSpace(cmd.UserID), videoID, name, payload, cmd.Priority, cmd.TimeLimit)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to create job")
}
return job, nil
}
func (m *Module) CancelAdminJob(ctx context.Context, cmd CancelAdminJobCommand) (*CancelAdminJobResult, error) {
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
return nil, err
}
videoService := m.runtime.VideoService()
if videoService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
if cmd.ID == "" {
return nil, status.Error(codes.NotFound, "Job not found")
}
if err := videoService.CancelJob(ctx, cmd.ID); err != nil {
if strings.Contains(strings.ToLower(err.Error()), "not found") {
return nil, status.Error(codes.NotFound, "Job not found")
}
return nil, status.Error(codes.FailedPrecondition, err.Error())
}
return &CancelAdminJobResult{Status: "cancelled", JobID: cmd.ID}, nil
}
func (m *Module) RetryAdminJob(ctx context.Context, cmd RetryAdminJobCommand) (*videodomain.Job, error) {
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
return nil, err
}
videoService := m.runtime.VideoService()
if videoService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
if cmd.ID == "" {
return nil, status.Error(codes.NotFound, "Job not found")
}
job, err := videoService.RetryJob(ctx, cmd.ID)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "not found") {
return nil, status.Error(codes.NotFound, "Job not found")
}
return nil, status.Error(codes.FailedPrecondition, err.Error())
}
return job, nil
}
func (m *Module) ListAdminAgents(ctx context.Context) ([]*videodomain.AgentWithStats, error) {
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
return nil, err
}
agentRuntime := m.runtime.AgentRuntime()
if agentRuntime == nil {
return nil, status.Error(codes.Unavailable, "Agent runtime is unavailable")
}
return agentRuntime.ListAgentsWithStats(), nil
}
func (m *Module) RestartAdminAgent(ctx context.Context, cmd AgentCommand) (string, error) {
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
return "", err
}
agentRuntime := m.runtime.AgentRuntime()
if agentRuntime == nil {
return "", status.Error(codes.Unavailable, "Agent runtime is unavailable")
}
if !agentRuntime.SendCommand(strings.TrimSpace(cmd.ID), cmd.Command) {
return "", status.Error(codes.Unavailable, "Agent not active or command channel full")
}
return cmd.Success, nil
}
func (m *Module) UpdateAdminAgent(ctx context.Context, cmd AgentCommand) (string, error) {
return m.RestartAdminAgent(ctx, cmd)
}

View File

@@ -0,0 +1,54 @@
package jobs
import (
appv1 "stream.api/internal/gen/proto/app/v1"
"stream.api/internal/modules/common"
videodomain "stream.api/internal/video"
)
func presentListAdminJobsResponse(result *ListAdminJobsResult) *appv1.ListAdminJobsResponse {
jobs := make([]*appv1.AdminJob, 0, len(result.Jobs))
for _, job := range result.Jobs {
jobs = append(jobs, common.BuildAdminJob(job))
}
response := &appv1.ListAdminJobsResponse{
Jobs: jobs,
Total: result.Total,
Offset: int32(result.Offset),
Limit: int32(result.Limit),
HasMore: result.HasMore,
PageSize: int32(result.PageSize),
}
if result.NextCursor != nil {
response.NextCursor = result.NextCursor
}
return response
}
func presentGetAdminJobResponse(job *videodomain.Job) *appv1.GetAdminJobResponse {
return &appv1.GetAdminJobResponse{Job: common.BuildAdminJob(job)}
}
func presentCreateAdminJobResponse(job *videodomain.Job) *appv1.CreateAdminJobResponse {
return &appv1.CreateAdminJobResponse{Job: common.BuildAdminJob(job)}
}
func presentCancelAdminJobResponse(result *CancelAdminJobResult) *appv1.CancelAdminJobResponse {
return &appv1.CancelAdminJobResponse{Status: result.Status, JobId: result.JobID}
}
func presentRetryAdminJobResponse(job *videodomain.Job) *appv1.RetryAdminJobResponse {
return &appv1.RetryAdminJobResponse{Job: common.BuildAdminJob(job)}
}
func presentListAdminAgentsResponse(items []*videodomain.AgentWithStats) *appv1.ListAdminAgentsResponse {
agents := make([]*appv1.AdminAgent, 0, len(items))
for _, item := range items {
agents = append(agents, common.BuildAdminAgent(item))
}
return &appv1.ListAdminAgentsResponse{Agents: agents}
}
func presentAgentCommandResponse(status string) *appv1.AdminAgentCommandResponse {
return &appv1.AdminAgentCommandResponse{Status: status}
}

View File

@@ -0,0 +1,56 @@
package jobs
import videodomain "stream.api/internal/video"
type ListAdminJobsQuery struct {
AgentID string
Offset int
Limit int
Cursor *string
PageSize int
UseCursorPagination bool
}
type ListAdminJobsResult struct {
Jobs []*videodomain.Job
Total int64
Offset int
Limit int
HasMore bool
PageSize int
NextCursor *string
}
type GetAdminJobQuery struct {
ID string
}
type CreateAdminJobCommand struct {
Command string
Image string
Name string
UserID string
VideoID *string
Env map[string]string
Priority int
TimeLimit int64
}
type CancelAdminJobCommand struct {
ID string
}
type CancelAdminJobResult struct {
Status string
JobID string
}
type RetryAdminJobCommand struct {
ID string
}
type AgentCommand struct {
ID string
Command string
Success string
}