// Package service provides business logic / use cases for the application. // This file contains shell and git command execution functionality. package service import ( "context" "encoding/json" "fmt" "time" "github.com/google/uuid" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" "github.com/orchard9/rdev/internal/sanitize" ) // ExecuteShellRequest contains parameters for running a shell command. type ExecuteShellRequest struct { ProjectID domain.ProjectID Command string StreamID string Audit *AuditContext // Optional audit context } // ExecuteShellResult contains the result of queuing a shell command. type ExecuteShellResult struct { CommandID domain.CommandID StreamURL string } // ExecuteShell runs a shell command in the project's pod. func (s *ProjectService) ExecuteShell(ctx context.Context, req ExecuteShellRequest) (*ExecuteShellResult, error) { // Validate project exists project, err := s.projects.Get(ctx, req.ProjectID) if err != nil { return nil, err } // Validate command if req.Command == "" { return nil, fmt.Errorf("%w: command is required", domain.ErrInvalidCommand) } if err := sanitize.ShellCommand(req.Command); err != nil { return nil, fmt.Errorf("%w: %w", domain.ErrCommandSanitization, err) } // Validate stream ID if err := sanitize.StreamID(req.StreamID); err != nil { return nil, fmt.Errorf("%w: %w", domain.ErrInvalidCommand, err) } // Generate command ID cmdNum := s.cmdID.Add(1) cmdID := domain.CommandID(fmt.Sprintf("cmd-%s-%03d", req.ProjectID, cmdNum)) if req.StreamID != "" { cmdID = domain.CommandID(req.StreamID) } // Create command cmd := &domain.Command{ ID: cmdID, ProjectID: req.ProjectID, Type: domain.CommandTypeShell, Args: []string{req.Command}, StartedAt: time.Now(), } // Log audit start if audit logger is configured if s.auditLogger != nil && req.Audit != nil { log := logging.FromContext(ctx).WithService("ProjectService") argsJSON, _ := json.Marshal(cmd.Args) auditEntry := &domain.AuditLogEntry{ ID: uuid.New().String(), APIKeyID: req.Audit.APIKeyID, CommandID: string(cmdID), ProjectID: string(req.ProjectID), CommandType: domain.CommandTypeShell, Args: string(argsJSON), ClientIP: req.Audit.ClientIP, UserAgent: req.Audit.UserAgent, StartedAt: cmd.StartedAt, Status: domain.AuditStatusRunning, } if err := s.auditLogger.LogCommandStart(ctx, auditEntry); err != nil { log.Warn("failed to log audit start", "command_id", cmdID, logging.FieldError, err) } } // Execute in background go s.executeCommand(project.PodName, cmd) return &ExecuteShellResult{ CommandID: cmdID, StreamURL: fmt.Sprintf("/projects/%s/events?stream_id=%s", req.ProjectID, cmdID), }, nil } // ExecuteGitRequest contains parameters for running a git command. type ExecuteGitRequest struct { ProjectID domain.ProjectID Args []string StreamID string Audit *AuditContext // Optional audit context } // ExecuteGitResult contains the result of queuing a git command. type ExecuteGitResult struct { CommandID domain.CommandID StreamURL string } // ExecuteGit runs a git command in the project's pod. func (s *ProjectService) ExecuteGit(ctx context.Context, req ExecuteGitRequest) (*ExecuteGitResult, error) { // Validate project exists project, err := s.projects.Get(ctx, req.ProjectID) if err != nil { return nil, err } // Validate args if len(req.Args) == 0 { return nil, fmt.Errorf("%w: args is required", domain.ErrInvalidCommand) } if err := sanitize.GitArgs(req.Args); err != nil { return nil, fmt.Errorf("%w: %w", domain.ErrCommandSanitization, err) } // Validate stream ID if err := sanitize.StreamID(req.StreamID); err != nil { return nil, fmt.Errorf("%w: %w", domain.ErrInvalidCommand, err) } // Generate command ID cmdNum := s.cmdID.Add(1) cmdID := domain.CommandID(fmt.Sprintf("cmd-%s-%03d", req.ProjectID, cmdNum)) if req.StreamID != "" { cmdID = domain.CommandID(req.StreamID) } // Create command cmd := &domain.Command{ ID: cmdID, ProjectID: req.ProjectID, Type: domain.CommandTypeGit, Args: req.Args, StartedAt: time.Now(), } // Log audit start if audit logger is configured if s.auditLogger != nil && req.Audit != nil { log := logging.FromContext(ctx).WithService("ProjectService") argsJSON, _ := json.Marshal(cmd.Args) auditEntry := &domain.AuditLogEntry{ ID: uuid.New().String(), APIKeyID: req.Audit.APIKeyID, CommandID: string(cmdID), ProjectID: string(req.ProjectID), CommandType: domain.CommandTypeGit, Args: string(argsJSON), ClientIP: req.Audit.ClientIP, UserAgent: req.Audit.UserAgent, StartedAt: cmd.StartedAt, Status: domain.AuditStatusRunning, } if err := s.auditLogger.LogCommandStart(ctx, auditEntry); err != nil { log.Warn("failed to log audit start", "command_id", cmdID, logging.FieldError, err) } } // Execute in background go s.executeCommand(project.PodName, cmd) return &ExecuteGitResult{ CommandID: cmdID, StreamURL: fmt.Sprintf("/projects/%s/events?stream_id=%s", req.ProjectID, cmdID), }, nil }