Ctrl K
ComunidadGuías…

main.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"context"
	"embed"
	"net/http"
	"os"
	"os/exec"
	"os/signal"
	"syscall"
	"time"

	"github.com/alexanderritik/byot/auth"
	"github.com/alexanderritik/byot/config"
	"github.com/alexanderritik/byot/db"
	"github.com/alexanderritik/byot/handler"
	"github.com/alexanderritik/byot/queue"
	"github.com/alexanderritik/byot/repository"
	"github.com/alexanderritik/byot/retention"
	"github.com/alexanderritik/byot/runtime"
	"github.com/alexanderritik/byot/scheduler"
	"github.com/alexanderritik/byot/secrets"
	"github.com/alexanderritik/byot/storage"
	"github.com/alexanderritik/byot/web"
	"github.com/alexanderritik/byot/worker"
	"github.com/google/uuid"

	"github.com/golang-migrate/migrate/v4"
	_ "github.com/golang-migrate/migrate/v4/database/postgres"
	"github.com/golang-migrate/migrate/v4/source/iofs"
	"github.com/rs/zerolog/log"
)

// Migrations are embedded so the binary needs no migrations/ folder at runtime.
//
//go:embed migrations/*.sql
var migrationFiles embed.FS

func runMigrations(dbURL string) error {
	src, err := iofs.New(migrationFiles, "migrations")
	if err != nil {
		return err
	}
	m, err := migrate.NewWithSourceInstance("iofs", src, dbURL)
	if err != nil {
		return err
	}
	if err := m.Up(); err != nil && err != migrate.ErrNoChange {
		return err
	}
	return nil
}

func pullImages() {
	for _, img := range runtime.Images {
		log.Info().Str("image", img).Msg("pulling")
		cmd := exec.Command("docker", "pull", img)
		cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
		if err := cmd.Run(); err != nil {
			log.Fatal().Err(err).Str("image", img).Msg("pull failed")
		}
	}
}

func main() {
	if len(os.Args) > 1 && os.Args[1] == "pull-images" {
		pullImages()
		return
	}

	cfg := config.LoadConfig()

	pool, err := db.Connect(cfg.DBUrl)
	if err != nil {
		log.Fatal().Err(err).Msg("failed to connect to postgres")
	}

	if os.Getenv("AUTO_MIGRATE") != "false" {
		if err := runMigrations(cfg.DBUrl); err != nil {
			log.Fatal().Err(err).Msg("migration failed (set AUTO_MIGRATE=false to manage migrations yourself)")
		}
		log.Info().Msg("database migrations up to date")
	}

	var store storage.Storage

	switch cfg.StorageProvider {

	case "minio":

		store, err = storage.NewMinioStorage(
			cfg.MinioEndpoint,
			cfg.MinioAccessKey,
			cfg.MinioSecretKey,
			cfg.MinioBucket,
			cfg.MinioRegion,
		)

	case "supabase":

		store, err = storage.NewSupabaseStorage(
			cfg.SupabaseURL,
			cfg.SupabaseKey,
			cfg.SupabaseBucket,
		)

	default:

		log.Fatal().
			Str(
				"provider",
				cfg.StorageProvider,
			).
			Msg("unsupported storage provider")
	}

	if err != nil {

		log.Fatal().
			Err(err).
			Str(
				"provider",
				cfg.StorageProvider,
			).
			Msg("failed to initialize storage")
	}

	log.Info().
		Str(
			"provider",
			cfg.StorageProvider,
		).
		Msg("storage initialized")

	verifier := auth.New(cfg.SupabaseURL, cfg.SupabasePublishableKey)
	if !verifier.Enabled() {
		log.Warn().Msg("SUPABASE_URL/SUPABASE_PUBLISHABLE_KEY not set: upload authentication is DISABLED")
	}

	box, err := secrets.New(cfg.SecretsKey)
	if err != nil {
		log.Fatal().Err(err).Msg("invalid SECRETS_KEY")
	}
	if !box.Enabled() {
		log.Warn().Msg("SECRETS_KEY not set: secret environment variables are DISABLED (plain variables still work)")
	}

	testRepo := repository.NewTestRepository(pool)
	envRepo := repository.NewEnvRepository(pool)
	testRunRepo := repository.NewTestRunRepository(pool)
	jobRepo := repository.NewJobRepository(pool)
	screenshotRepo := repository.NewScreenshotRepository(pool)

	q := queue.NewQueue(jobRepo)

	sched := scheduler.NewScheduler(testRepo, q, jobRepo, 30*time.Second)
	handle := handler.NewHandler(store, testRepo, testRunRepo, screenshotRepo, q, sched, cfg, envRepo, box, verifier.Require)

	// Create and start worker
	workerID := uuid.NewString()
	w := worker.NewWorker(workerID, q, testRepo, testRunRepo, screenshotRepo, store, envRepo, box, cfg.WorkerConcurrency)
	workerCtx, stopWorker := context.WithCancel(context.Background())
	defer stopWorker()

	workerDone := make(chan struct{})
	go func() {
		w.Start(workerCtx)
		close(workerDone)
	}()
	go sched.Start(workerCtx)

	sweeper := retention.New(repository.NewRetentionRepository(pool), store, retention.Policy{
		MaxRunsPerTest: cfg.RetentionMaxRuns,
		MaxAge:         time.Duration(cfg.RetentionDays) * 24 * time.Hour,
	})
	go sweeper.Run(workerCtx, time.Hour)

	mux := http.NewServeMux()
	mux.HandleFunc("/auth/config", verifier.PublicConfig)

	web.Register(mux)
	mux.HandleFunc("/health", verifier.RequireWrites(handle.IsHealth))
	mux.HandleFunc("/uploadBinary", verifier.RequireWrites(handle.UploadBinary))
	mux.HandleFunc("/run", verifier.RequireWrites(handle.Run))
	mux.HandleFunc("/stop", verifier.RequireWrites(handle.Stop))
	mux.HandleFunc("/scheduler/status", verifier.RequireWrites(handle.SchedulerStatus))
	mux.HandleFunc("/scheduler/stop", verifier.RequireWrites(handle.SchedulerStop))
	mux.HandleFunc("/scheduler/start", verifier.RequireWrites(handle.SchedulerStart))
	mux.HandleFunc("/tests", verifier.RequireWrites(handle.Tests))
	mux.HandleFunc("/tests/", verifier.RequireWrites(handle.Tests))
	mux.HandleFunc("/runs/", verifier.RequireWrites(handle.RunRoutes))
	mux.HandleFunc("/status/", verifier.RequireWrites(handle.JobStatus))
	mux.HandleFunc("/screenshots/", verifier.RequireWrites(handle.ListScreenshots))
	mux.HandleFunc("/screenshot/", verifier.RequireWrites(handle.DownloadScreenshot))

	server := &http.Server{
		Addr:    ":3000",
		Handler: mux,
	}

	go func() {
		log.Info().Msg("server listening on :3000")

		if err := server.ListenAndServe(); err != nil &&
			err != http.ErrServerClosed {
			log.Fatal().Err(err).Msg("server failed")
		}
	}()

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

	sig := <-sigChan
	log.Info().Str("signal", sig.String()).Msg("shutdown initiated")

	// Allow active requests to finish
	shutdownCtx, cancel := context.WithTimeout(
		context.Background(),
		30*time.Second,
	)
	defer cancel()

	if err := server.Shutdown(shutdownCtx); err != nil {
		log.Error().Err(err).Msg("http shutdown failed")
	}

	// Stop claiming jobs and let running ones finish (bounded by the same
	// deadline). Anything still running is left for stale-job recovery.
	stopWorker()
	select {
	case <-workerDone:
	case <-shutdownCtx.Done():
		log.Warn().Msg("shutdown deadline hit with jobs still running")
	}

	// Close postgres pool
	pool.Close()

	log.Info().Msg("shutdown complete")
}