Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,19 @@ jobs:
--health-retries 5
ports:
- 5432:5432

docker:
image: docker:27-dind
env:
DOCKER_TLS_CERTDIR: /certs
options: >-
--privileged
--health-cmd "docker info"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 2376:2376

steps:
- name: Checkout code
Expand Down Expand Up @@ -176,6 +189,8 @@ jobs:
TEST_DB_NAME: voidrunner_test
TEST_DB_SSLMODE: disable
JWT_SECRET_KEY: test-secret-key-for-integration
DOCKER_HOST: tcp://localhost:2376
DOCKER_TLS_VERIFY: 0
run: make test-integration

docs:
Expand Down
143 changes: 142 additions & 1 deletion cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"

Expand All @@ -43,6 +45,8 @@ import (
"github.com/voidrunnerhq/voidrunner/internal/auth"
"github.com/voidrunnerhq/voidrunner/internal/config"
"github.com/voidrunnerhq/voidrunner/internal/database"
"github.com/voidrunnerhq/voidrunner/internal/executor"
"github.com/voidrunnerhq/voidrunner/internal/services"
"github.com/voidrunnerhq/voidrunner/pkg/logger"
)

Expand Down Expand Up @@ -95,12 +99,111 @@ func main() {
// Initialize authentication service
authService := auth.NewService(repos.Users, jwtService, log.Logger, cfg)

// Initialize executor configuration
executorConfig := &executor.Config{
DockerEndpoint: cfg.Executor.DockerEndpoint,
DefaultResourceLimits: executor.ResourceLimits{
MemoryLimitBytes: int64(cfg.Executor.DefaultMemoryLimitMB) * 1024 * 1024,
CPUQuota: cfg.Executor.DefaultCPUQuota,
PidsLimit: cfg.Executor.DefaultPidsLimit,
TimeoutSeconds: cfg.Executor.DefaultTimeoutSeconds,
},
DefaultTimeoutSeconds: cfg.Executor.DefaultTimeoutSeconds,
Images: executor.ImageConfig{
Python: cfg.Executor.PythonImage,
Bash: cfg.Executor.BashImage,
JavaScript: cfg.Executor.JavaScriptImage,
Go: cfg.Executor.GoImage,
},
Security: executor.SecuritySettings{
EnableSeccomp: cfg.Executor.EnableSeccomp,
SeccompProfilePath: cfg.Executor.SeccompProfilePath,
EnableAppArmor: cfg.Executor.EnableAppArmor,
AppArmorProfile: cfg.Executor.AppArmorProfile,
ExecutionUser: cfg.Executor.ExecutionUser,
},
}

// Create seccomp profile directory if it doesn't exist
if cfg.Executor.EnableSeccomp {
seccompDir := filepath.Dir(cfg.Executor.SeccompProfilePath)
if err := os.MkdirAll(seccompDir, 0750); err != nil {
log.Warn("failed to create seccomp profile directory", "error", err, "path", seccompDir)
}

// Create a temporary security manager to generate the seccomp profile
tempSecurityManager := executor.NewSecurityManager(executorConfig)
seccompProfilePath, err := tempSecurityManager.CreateSeccompProfile(context.Background())
if err != nil {
log.Warn("failed to create seccomp profile", "error", err)
} else {
// Copy the profile to the configured location
if seccompProfilePath != cfg.Executor.SeccompProfilePath {
if err := copyFile(seccompProfilePath, cfg.Executor.SeccompProfilePath); err != nil {
log.Warn("failed to copy seccomp profile to configured location", "error", err)
} else {
log.Info("seccomp profile created successfully", "path", cfg.Executor.SeccompProfilePath)
}
// Clean up temporary profile
_ = os.Remove(seccompProfilePath)
} else {
log.Info("seccomp profile created successfully", "path", cfg.Executor.SeccompProfilePath)
}
}
}

// Initialize executor (Docker or Mock based on availability)
var taskExecutor executor.TaskExecutor

// Try to initialize Docker executor first
dockerExecutor, err := executor.NewExecutor(executorConfig, log.Logger)
if err != nil {
log.Warn("failed to initialize Docker executor, falling back to mock executor", "error", err)
// Use mock executor for environments without Docker (e.g., CI)
taskExecutor = executor.NewMockExecutor(executorConfig, log.Logger)
log.Info("mock executor initialized successfully")
} else {
// Check Docker executor health
healthCtx, healthCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer healthCancel()

if err := dockerExecutor.IsHealthy(healthCtx); err != nil {
log.Warn("Docker executor health check failed, falling back to mock executor", "error", err)
// Cleanup failed Docker executor
_ = dockerExecutor.Cleanup(context.Background())
// Use mock executor instead
taskExecutor = executor.NewMockExecutor(executorConfig, log.Logger)
log.Info("mock executor initialized successfully")
} else {
taskExecutor = dockerExecutor
log.Info("Docker executor initialized successfully")
// Add cleanup for successful Docker executor
defer func() {
if err := dockerExecutor.Cleanup(context.Background()); err != nil {
log.Error("failed to cleanup Docker executor", "error", err)
}
}()
}
}

// Initialize task execution service
taskExecutionService := services.NewTaskExecutionService(dbConn, log.Logger)

// Initialize task executor service
taskExecutorService := services.NewTaskExecutorService(
taskExecutionService,
repos.Tasks,
taskExecutor,
nil, // cleanup manager will be initialized within the executor
log.Logger,
)

if cfg.IsProduction() {
gin.SetMode(gin.ReleaseMode)
}

router := gin.New()
routes.Setup(router, cfg, log, dbConn, repos, authService)
routes.Setup(router, cfg, log, dbConn, repos, authService, taskExecutionService, taskExecutorService)

srv := &http.Server{
Addr: fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port),
Expand Down Expand Up @@ -140,3 +243,41 @@ func main() {

log.Info("server exited")
}

// copyFile copies a file from src to dst with proper path validation
func copyFile(src, dst string) error {
// Validate and clean paths to prevent directory traversal
cleanSrc := filepath.Clean(src)
cleanDst := filepath.Clean(dst)

// Additional security check: ensure paths don't contain ".." or other suspicious patterns
if !filepath.IsAbs(cleanSrc) || !filepath.IsAbs(cleanDst) {
return fmt.Errorf("paths must be absolute")
}
// #nosec G304 - Path traversal mitigation: paths are validated and cleaned above
sourceFile, err := os.Open(cleanSrc)
if err != nil {
return err
}
defer sourceFile.Close()

// Ensure destination directory exists
if err := os.MkdirAll(filepath.Dir(cleanDst), 0750); err != nil {
return err
}

// #nosec G304 - Path traversal mitigation: paths are validated and cleaned above
destFile, err := os.Create(cleanDst)
if err != nil {
return err
}
defer destFile.Close()

_, err = io.Copy(destFile, sourceFile)
if err != nil {
return err
}

// Set file permissions to 0600 for security
return os.Chmod(cleanDst, 0600)
}
Loading