123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584 |
- package services
- import (
- "context"
- "fmt"
- "os"
- "os/exec"
- "strings"
- "sync"
- "time"
- "git.linuxforward.com/byop/byop-engine/clients"
- "git.linuxforward.com/byop/byop-engine/cloud"
- "git.linuxforward.com/byop/byop-engine/config"
- "git.linuxforward.com/byop/byop-engine/dbstore"
- "git.linuxforward.com/byop/byop-engine/models"
- "github.com/sirupsen/logrus"
- )
- // RemotePreviewService handles remote VPS preview deployments
- type RemotePreviewService struct {
- common *PreviewCommon
- entry *logrus.Entry
- ovhProvider cloud.Provider
- config *config.Config
- }
- // NewRemotePreviewService creates a new RemotePreviewService
- func NewRemotePreviewService(store *dbstore.SQLiteStore, ovhProvider cloud.Provider, cfg *config.Config, registryClient clients.RegistryClient, registryURL, registryUser, registryPass string) *RemotePreviewService {
- return &RemotePreviewService{
- common: NewPreviewCommon(store, registryClient, registryURL, registryUser, registryPass),
- entry: logrus.WithField("service", "RemotePreviewService"),
- ovhProvider: ovhProvider,
- config: cfg,
- }
- }
- // Close cleans up resources
- func (rps *RemotePreviewService) Close(ctx context.Context) {
- rps.entry.Info("Cleaning up remote preview service...")
- rps.cleanupAllPreviewVPS(ctx)
- rps.common.Close()
- }
- // cleanupAllPreviewVPS cleans up all preview VPS instances on server shutdown
- func (rps *RemotePreviewService) cleanupAllPreviewVPS(ctx context.Context) {
- rps.entry.Info("Starting cleanup of all preview VPS instances")
- // Get all VPS instances
- instances, err := rps.ovhProvider.ListInstances(ctx)
- if err != nil {
- rps.entry.WithError(err).Error("Failed to list VPS instances during cleanup")
- return
- }
- var wg sync.WaitGroup
- cleanupSemaphore := make(chan struct{}, 3) // Limit concurrent cleanup operations
- for _, instance := range instances {
- // Only clean up preview VPS instances
- if strings.Contains(instance.Name, "preview.byop.fr") {
- wg.Add(1)
- go func(inst cloud.Instance) {
- defer wg.Done()
- cleanupSemaphore <- struct{}{} // Acquire semaphore
- defer func() { <-cleanupSemaphore }() // Release semaphore
- taskCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
- defer cancel()
- rps.entry.WithField("vps_id", inst.ID).WithField("vps_name", inst.Name).Info("Cleaning up preview VPS")
- // Stop all containers and clean up Docker images
- if inst.IPAddress != "" {
- rps.cleanupVPSResources(taskCtx, inst.IPAddress, inst.ID)
- }
- // Reset/destroy the VPS instance
- if err := rps.ovhProvider.ResetInstance(taskCtx, inst.ID); err != nil {
- rps.entry.WithField("vps_id", inst.ID).WithError(err).Error("Failed to reset preview VPS")
- } else {
- rps.entry.WithField("vps_id", inst.ID).Info("Successfully reset preview VPS")
- }
- }(instance)
- }
- }
- // Wait for all cleanup operations to complete with a timeout
- done := make(chan struct{})
- go func() {
- wg.Wait()
- close(done)
- }()
- select {
- case <-done:
- rps.entry.Info("Successfully completed cleanup of all preview VPS instances")
- case <-time.After(60 * time.Second):
- rps.entry.Warn("Timeout waiting for preview VPS cleanup to complete")
- }
- }
- // cleanupVPSResources cleans up all Docker resources on a VPS
- func (rps *RemotePreviewService) cleanupVPSResources(ctx context.Context, ipAddress, vpsID string) {
- rps.entry.WithField("vps_id", vpsID).WithField("ip_address", ipAddress).Info("Cleaning up Docker resources on VPS")
- // Stop all preview containers
- stopAllCmd := "docker ps -q --filter 'label=byop.preview=true' | xargs -r docker stop"
- if err := rps.common.executeSSHCommand(ctx, ipAddress, stopAllCmd); err != nil {
- rps.entry.WithField("vps_id", vpsID).WithError(err).Warn("Failed to stop preview containers")
- }
- // Remove all preview containers
- removeAllCmd := "docker ps -aq --filter 'label=byop.preview=true' | xargs -r docker rm -f"
- if err := rps.common.executeSSHCommand(ctx, ipAddress, removeAllCmd); err != nil {
- rps.entry.WithField("vps_id", vpsID).WithError(err).Warn("Failed to remove preview containers")
- }
- // Remove all BYOP preview images
- removeImagesCmd := "docker images --filter 'reference=byop-preview-*' -q | xargs -r docker rmi -f"
- if err := rps.common.executeSSHCommand(ctx, ipAddress, removeImagesCmd); err != nil {
- rps.entry.WithField("vps_id", vpsID).WithError(err).Warn("Failed to remove preview images")
- }
- // Clean up all project directories
- cleanupDirsCmd := "rm -rf /home/debian/preview-*"
- if err := rps.common.executeSSHCommand(ctx, ipAddress, cleanupDirsCmd); err != nil {
- rps.entry.WithField("vps_id", vpsID).WithError(err).Warn("Failed to clean up project directories")
- }
- // Clean up temporary tar files
- cleanupTarCmd := "rm -f /tmp/byop-preview-*.tar"
- if err := rps.common.executeSSHCommand(ctx, ipAddress, cleanupTarCmd); err != nil {
- rps.entry.WithField("vps_id", vpsID).WithError(err).Warn("Failed to clean up tar files")
- }
- // Final cleanup: remove dangling images and volumes
- pruneCmd := "docker system prune -af --volumes"
- if err := rps.common.executeSSHCommand(ctx, ipAddress, pruneCmd); err != nil {
- rps.entry.WithField("vps_id", vpsID).WithError(err).Warn("Failed to prune Docker system")
- }
- rps.entry.WithField("vps_id", vpsID).Info("Completed Docker resource cleanup on VPS")
- }
- // CreatePreview creates a remote preview environment on a VPS
- func (rps *RemotePreviewService) CreatePreview(ctx context.Context, appId int) (*models.Preview, error) {
- // Get app details
- app, err := rps.common.GetStore().GetAppByID(ctx, appId)
- if err != nil {
- return nil, fmt.Errorf("failed to get app by ID %d: %v", appId, err)
- }
- // Create preview record
- preview := models.Preview{
- AppID: app.ID,
- Status: "building",
- ExpiresAt: time.Now().Add(24 * time.Hour).Format(time.RFC3339), // 24h expiry
- }
- previewID, err := rps.common.GetStore().CreatePreview(ctx, &preview)
- if err != nil {
- return nil, fmt.Errorf("failed to create preview record: %v", err)
- }
- preview.ID = previewID
- // Start async build and deploy to VPS
- go rps.buildAndDeployPreview(ctx, preview, app)
- return &preview, nil
- }
- func (rps *RemotePreviewService) buildAndDeployPreview(ctx context.Context, preview models.Preview, app *models.App) {
- rps.entry.WithField("preview_id", preview.ID).Info("Starting remote preview build and deployment")
- // Get all components for the app
- rps.entry.WithField("preview_id", preview.ID).Info("Getting app components")
- components, err := rps.common.GetAppComponents(ctx, app)
- if err != nil {
- rps.entry.WithField("preview_id", preview.ID).Errorf("Failed to get app components: %v", err)
- rps.common.UpdatePreviewStatus(ctx, preview.ID, "failed", fmt.Sprintf("Failed to get app components: %v", err))
- rps.common.GetStore().UpdateAppStatus(ctx, app.ID, "failed", fmt.Sprintf("Preview creation failed: %v", err))
- return
- }
- rps.entry.WithField("preview_id", preview.ID).WithField("component_count", len(components)).Info("Successfully retrieved app components")
- // Step 1: Build Docker images locally
- rps.entry.WithField("preview_id", preview.ID).Info("Starting Docker image build phase")
- imageNames, buildLogs, err := rps.common.BuildComponentImages(ctx, components)
- if err != nil {
- rps.entry.WithField("preview_id", preview.ID).Errorf("Failed to build component images: %v", err)
- rps.common.UpdatePreviewStatus(ctx, preview.ID, "failed", fmt.Sprintf("Failed to build images: %v", err))
- rps.common.UpdatePreviewBuildLogs(ctx, preview.ID, buildLogs)
- rps.common.GetStore().UpdateAppStatus(ctx, app.ID, "failed", fmt.Sprintf("Preview build failed: %v", err))
- return
- }
- rps.entry.WithField("preview_id", preview.ID).WithField("image_count", len(imageNames)).Info("Docker image build phase completed successfully")
- rps.common.UpdatePreviewBuildLogs(ctx, preview.ID, buildLogs)
- // Step 2: Provision preview VPS
- rps.entry.WithField("preview_id", preview.ID).Info("Starting VPS provisioning phase")
- rps.common.UpdatePreviewStatus(ctx, preview.ID, "deploying", "")
- vps, err := rps.findAvailablePreviewVPS(ctx)
- if err != nil {
- rps.entry.WithField("preview_id", preview.ID).Errorf("Failed to find available VPS: %v", err)
- rps.common.UpdatePreviewStatus(ctx, preview.ID, "failed", fmt.Sprintf("Failed to find available VPS: %v", err))
- rps.common.GetStore().UpdateAppStatus(ctx, app.ID, "failed", fmt.Sprintf("Preview VPS provisioning failed: %v", err))
- return
- }
- vpsID := vps.ID
- ipAddress := vps.IPAddress
- rps.entry.WithField("preview_id", preview.ID).WithField("vps_id", vpsID).WithField("ip_address", ipAddress).Info("VPS provisioning completed")
- // Generate preview URL with UUID
- previewUUID := rps.common.GeneratePreviewID()
- previewTLD := rps.config.PreviewTLD
- previewURL := fmt.Sprintf("https://%s.%s", previewUUID, previewTLD)
- rps.entry.WithField("preview_id", preview.ID).WithField("preview_uuid", previewUUID).WithField("preview_url", previewURL).Info("Generated remote preview URL")
- // Update preview with VPS info
- if err := rps.common.GetStore().UpdatePreviewVPS(ctx, preview.ID, vpsID, ipAddress, previewURL); err != nil {
- rps.entry.WithField("preview_id", preview.ID).Errorf("Failed to update preview VPS info: %v", err)
- }
- // Step 3: Deploy to VPS
- rps.entry.WithField("preview_id", preview.ID).Info("Starting remote deployment phase")
- deployLogs, err := rps.deployToVPS(ctx, ipAddress, imageNames, app, preview.ID, previewUUID)
- if err != nil {
- rps.entry.WithField("preview_id", preview.ID).Errorf("Failed to deploy to VPS: %v", err)
- rps.common.UpdatePreviewStatus(ctx, preview.ID, "failed", fmt.Sprintf("Failed to deploy to VPS: %v", err))
- rps.common.UpdatePreviewDeployLogs(ctx, preview.ID, deployLogs)
- rps.common.GetStore().UpdateAppStatus(ctx, app.ID, "failed", fmt.Sprintf("Remote deployment failed: %v", err))
- return
- }
- rps.entry.WithField("preview_id", preview.ID).Info("Remote deployment completed successfully")
- rps.common.UpdatePreviewDeployLogs(ctx, preview.ID, deployLogs)
- rps.common.UpdatePreviewStatus(ctx, preview.ID, "running", "")
- // Update app status to ready with preview info
- rps.common.GetStore().UpdateAppPreview(ctx, app.ID, preview.ID, previewURL)
- rps.entry.WithField("preview_id", preview.ID).WithField("vps_id", vpsID).WithField("preview_url", previewURL).Info("Remote preview deployment completed successfully")
- }
- // findAvailablePreviewVPS finds an existing VPS that can accommodate more previews
- func (rps *RemotePreviewService) findAvailablePreviewVPS(ctx context.Context) (*cloud.Instance, error) {
- // Get all VPS instances
- instances, err := rps.ovhProvider.ListInstances(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to list VPS instances: %v", err)
- }
- // Count previews per VPS by checking all preview records
- vpsUsage := make(map[string]int)
- // Get all preview instances from database and count usage per VPS
- for _, instance := range instances {
- if strings.Contains(instance.Name, "preview.byop.fr") {
- vpsUsage[instance.ID] = 0 // Initialize to 0, will be updated below
- }
- }
- // Simple approach: look for any preview VPS that exists and return it
- maxPreviewsPerVPS := 5
- // Check if any existing VPS has capacity
- for _, instance := range instances {
- if strings.Contains(instance.Name, "preview.byop.fr") {
- currentCount := vpsUsage[instance.ID]
- if currentCount < maxPreviewsPerVPS {
- rps.entry.WithField("vps_id", instance.ID).WithField("current_previews", currentCount).Info("Found VPS with available capacity")
- return &instance, nil
- }
- }
- }
- return nil, fmt.Errorf("no available VPS with capacity found")
- }
- func (rps *RemotePreviewService) deployToVPS(ctx context.Context, ipAddress string, imageNames []string, app *models.App, previewID int, previewUUID string) (string, error) {
- var logs strings.Builder
- rps.entry.WithField("ip_address", ipAddress).WithField("app_name", app.Name).WithField("preview_id", previewID).Info("Starting deployment to VPS")
- // Generate docker-compose.yml for the preview
- rps.entry.Info("Generating docker-compose.yml for preview")
- composeContent, err := rps.generatePreviewDockerCompose(imageNames, app, previewID, previewUUID)
- if err != nil {
- rps.entry.WithError(err).Error("Failed to generate docker-compose.yml")
- return logs.String(), err
- }
- logs.WriteString("Generated docker-compose.yml\n")
- // Save images to tar files and transfer to VPS
- for _, imageName := range imageNames {
- tarFile := fmt.Sprintf("/tmp/%s.tar", strings.ReplaceAll(imageName, ":", "_"))
- // Save Docker image
- rps.entry.WithField("image_name", imageName).WithField("tar_file", tarFile).Info("Saving Docker image to tar file")
- cmd := exec.CommandContext(ctx, "docker", "save", "-o", tarFile, imageName)
- if err := cmd.Run(); err != nil {
- rps.entry.WithField("image_name", imageName).WithError(err).Error("Failed to save image to tar")
- logs.WriteString(fmt.Sprintf("Failed to save image %s: %v\n", imageName, err))
- return logs.String(), err
- }
- rps.entry.WithField("image_name", imageName).Info("Successfully saved image to tar")
- // Transfer to VPS
- rps.entry.WithField("image_name", imageName).WithField("ip_address", ipAddress).Info("Transferring image to VPS")
- if err := rps.transferFile(ctx, tarFile, fmt.Sprintf("%s:/tmp/", ipAddress)); err != nil {
- rps.entry.WithField("image_name", imageName).WithError(err).Error("Failed to transfer image to VPS")
- logs.WriteString(fmt.Sprintf("Failed to transfer image %s: %v\n", imageName, err))
- return logs.String(), err
- }
- rps.entry.WithField("image_name", imageName).Info("Successfully transferred image to VPS")
- // Load image on VPS
- loadCmd := fmt.Sprintf("docker load -i /tmp/%s.tar", strings.ReplaceAll(imageName, ":", "_"))
- rps.entry.WithField("image_name", imageName).WithField("command", loadCmd).Info("Loading image on VPS")
- if err := rps.common.executeSSHCommand(ctx, ipAddress, loadCmd); err != nil {
- rps.entry.WithField("image_name", imageName).WithError(err).Error("Failed to load image on VPS")
- logs.WriteString(fmt.Sprintf("Failed to load image %s on VPS: %v\n", imageName, err))
- return logs.String(), err
- }
- rps.entry.WithField("image_name", imageName).Info("Successfully loaded image on VPS")
- // Clean up local tar file
- os.Remove(tarFile)
- rps.entry.WithField("tar_file", tarFile).Info("Cleaned up local tar file")
- }
- // Create project-specific directory and transfer docker-compose.yml
- projectName := fmt.Sprintf("preview-%d", previewID)
- projectDir := fmt.Sprintf("/home/debian/%s", projectName)
- // Create project directory on VPS
- rps.entry.WithField("project_dir", projectDir).Info("Creating project directory on VPS")
- if err := rps.common.executeSSHCommand(ctx, ipAddress, fmt.Sprintf("mkdir -p %s", projectDir)); err != nil {
- rps.entry.WithError(err).Error("Failed to create project directory on VPS")
- return logs.String(), err
- }
- composeFile := "/tmp/docker-compose-preview.yml"
- rps.entry.WithField("compose_file", composeFile).Info("Writing docker-compose.yml to temporary file")
- if err := os.WriteFile(composeFile, []byte(composeContent), 0644); err != nil {
- rps.entry.WithError(err).Error("Failed to write docker-compose.yml")
- return logs.String(), err
- }
- rps.entry.WithField("ip_address", ipAddress).WithField("project_dir", projectDir).Info("Transferring docker-compose.yml to VPS")
- if err := rps.transferFile(ctx, composeFile, fmt.Sprintf("%s:%s/docker-compose.yml", ipAddress, projectDir)); err != nil {
- rps.entry.WithError(err).Error("Failed to transfer docker-compose.yml to VPS")
- logs.WriteString(fmt.Sprintf("Failed to transfer docker-compose.yml: %v\n", err))
- return logs.String(), err
- }
- rps.entry.Info("Successfully transferred docker-compose.yml to VPS")
- // Start services on VPS with project-specific naming
- rps.entry.WithField("ip_address", ipAddress).WithField("project_name", projectName).Info("Starting services on VPS with docker-compose")
- startCmd := fmt.Sprintf("cd %s && docker compose -p %s up -d", projectDir, projectName)
- if err := rps.common.executeSSHCommand(ctx, ipAddress, startCmd); err != nil {
- rps.entry.WithError(err).Error("Failed to start services on VPS")
- logs.WriteString(fmt.Sprintf("Failed to start services: %v\n", err))
- return logs.String(), err
- }
- rps.entry.Info("Successfully started services on VPS")
- // Validate DNS and certificate setup for debugging
- previewDomain := fmt.Sprintf("%s.%s", previewUUID, rps.config.PreviewTLD)
- rps.validateDNSAndCertificate(ctx, previewDomain, ipAddress)
- rps.entry.WithField("ip_address", ipAddress).WithField("project_name", projectName).Info("Remote preview deployment completed successfully")
- logs.WriteString("Remote preview deployment completed successfully\n")
- return logs.String(), nil
- }
- func (rps *RemotePreviewService) generatePreviewDockerCompose(imageNames []string, app *models.App, previewID int, previewUUID string) (string, error) {
- rps.entry.WithField("app_id", app.ID).WithField("preview_id", previewID).WithField("image_count", len(imageNames)).Info("Generating docker-compose content for remote deployment")
- compose := "services:\n"
- for i, imageName := range imageNames {
- serviceName := fmt.Sprintf("service-%d", i)
- compose += fmt.Sprintf(" %s:\n", serviceName)
- compose += fmt.Sprintf(" image: %s\n", imageName)
- compose += " restart: unless-stopped\n"
- compose += " environment:\n"
- compose += " - NODE_ENV=preview\n"
- compose += fmt.Sprintf(" - APP_NAME=%s\n", app.Name)
- // Add BYOP preview labels for tracking
- compose += " labels:\n"
- compose += " - \"byop.preview=true\"\n"
- compose += fmt.Sprintf(" - \"byop.app.id=%d\"\n", app.ID)
- compose += fmt.Sprintf(" - \"byop.app.name=%s\"\n", app.Name)
- compose += fmt.Sprintf(" - \"byop.preview.id=%d\"\n", previewID)
- // Add Traefik labels for the first service (main entry point)
- if i == 0 {
- previewDomain := fmt.Sprintf("%s.%s", previewUUID, rps.config.PreviewTLD)
- routerName := fmt.Sprintf("preview-%d-%s", previewID, previewUUID)
- compose += " - \"traefik.enable=true\"\n"
- compose += fmt.Sprintf(" - \"traefik.http.routers.%s.rule=Host(`%s`)\"\n", routerName, previewDomain)
- compose += fmt.Sprintf(" - \"traefik.http.routers.%s.entrypoints=websecure\"\n", routerName)
- compose += fmt.Sprintf(" - \"traefik.http.routers.%s.tls=true\"\n", routerName)
- compose += fmt.Sprintf(" - \"traefik.http.routers.%s.tls.certresolver=tlsresolver\"\n", routerName)
- compose += " - \"traefik.docker.network=traefik\"\n"
- }
- compose += " networks:\n"
- compose += " - traefik\n"
- compose += "\n"
- }
- // Add networks section
- compose += "networks:\n"
- compose += " traefik:\n"
- compose += " external: true\n"
- return compose, nil
- }
- // DeletePreview deletes a remote preview
- func (rps *RemotePreviewService) DeletePreview(ctx context.Context, appID int) error {
- // Get the preview to ensure it exists
- preview, err := rps.common.GetStore().GetPreviewByAppID(ctx, appID)
- if err != nil {
- return fmt.Errorf("failed to get preview by app ID %d: %v", appID, err)
- }
- if preview == nil {
- return fmt.Errorf("preview with app ID %d not found", appID)
- }
- rps.entry.WithField("preview_id", preview.ID).Info("Deleting remote preview")
- // Stop and remove containers on VPS using project-specific naming
- if preview.IPAddress != "" && preview.IPAddress != "127.0.0.1" {
- projectName := fmt.Sprintf("preview-%d", preview.ID)
- projectDir := fmt.Sprintf("/home/debian/%s", projectName)
- // Stop containers with project-specific naming
- stopCmd := fmt.Sprintf("cd %s && docker compose -p %s down --remove-orphans", projectDir, projectName)
- rps.common.executeSSHCommand(ctx, preview.IPAddress, stopCmd)
- // Remove project directory
- rmCmd := fmt.Sprintf("rm -rf %s", projectDir)
- rps.common.executeSSHCommand(ctx, preview.IPAddress, rmCmd)
- // Cleanup Docker images
- if err := rps.common.CleanupPreviewImagesForApp(ctx, appID, true, preview.IPAddress); err != nil {
- rps.entry.WithField("preview_id", preview.ID).WithError(err).Warn("Failed to clean up Docker images")
- }
- }
- // Don't remove the VPS instance - it might be hosting other previews
- // Only clean up if this is the last preview on the VPS
- if preview.VPSID != "" && !strings.Contains(preview.VPSID, "byop.local") {
- // Check if there are other active previews on this VPS
- otherPreviews, err := rps.getActivePreviewsOnVPS(ctx, preview.VPSID)
- if err != nil {
- rps.entry.WithField("vps_id", preview.VPSID).Warnf("Failed to check other previews on VPS: %v", err)
- } else if len(otherPreviews) <= 1 { // Only this preview remains
- if err := rps.ovhProvider.ResetInstance(ctx, preview.VPSID); err != nil {
- rps.entry.WithField("vps_id", preview.VPSID).Errorf("Failed to reset VPS instance: %v", err)
- }
- }
- }
- // Delete the preview record from the database
- if err := rps.common.GetStore().DeletePreview(ctx, preview.ID); err != nil {
- return fmt.Errorf("failed to delete preview from database: %v", err)
- }
- rps.entry.WithField("preview_id", preview.ID).Info("Successfully deleted remote preview")
- return nil
- }
- // StopPreview stops a remote preview
- func (rps *RemotePreviewService) StopPreview(ctx context.Context, previewID int) error {
- preview, err := rps.common.GetStore().GetPreviewByID(ctx, previewID)
- if err != nil {
- return err
- }
- if preview == nil {
- return fmt.Errorf("preview not found")
- }
- // Stop containers on VPS
- projectName := fmt.Sprintf("preview-%d", previewID)
- projectDir := fmt.Sprintf("/home/debian/%s", projectName)
- stopCmd := fmt.Sprintf("cd %s && docker compose -p %s down --remove-orphans", projectDir, projectName)
- rps.common.executeSSHCommand(ctx, preview.IPAddress, stopCmd)
- // Clean up Docker images before destroying VPS
- if err := rps.common.CleanupPreviewImagesForApp(ctx, preview.AppID, true, preview.IPAddress); err != nil {
- rps.entry.WithField("preview_id", previewID).WithError(err).Warn("Failed to clean up Docker images")
- }
- // Destroy the VPS
- if err := rps.ovhProvider.ResetInstance(ctx, preview.VPSID); err != nil {
- rps.entry.WithField("vps_id", preview.VPSID).Errorf("Failed to destroy preview VPS: %v", err)
- }
- return rps.common.GetStore().UpdatePreviewStatus(ctx, previewID, "stopped", "")
- }
- // getActivePreviewsOnVPS returns all active previews running on a specific VPS
- func (rps *RemotePreviewService) getActivePreviewsOnVPS(ctx context.Context, vpsID string) ([]*models.Preview, error) {
- apps, err := rps.common.GetStore().GetAllApps(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to get apps: %v", err)
- }
- var activePreviews []*models.Preview
- for _, app := range apps {
- previews, err := rps.common.GetStore().GetPreviewsByAppID(ctx, app.ID)
- if err != nil {
- continue
- }
- for _, preview := range previews {
- if preview.VPSID == vpsID && preview.Status == "running" {
- activePreviews = append(activePreviews, preview)
- }
- }
- }
- return activePreviews, nil
- }
- // Helper methods - using common SSH command execution
- func (rps *RemotePreviewService) transferFile(ctx context.Context, localPath, remotePath string) error {
- hostAndPath := strings.SplitN(remotePath, ":", 2)
- if len(hostAndPath) != 2 {
- return fmt.Errorf("invalid SCP destination format: %s", remotePath)
- }
- remoteHost := hostAndPath[0]
- remotePath = hostAndPath[1]
- cmd := exec.CommandContext(ctx, "scp", "-o", "StrictHostKeyChecking=no", localPath, fmt.Sprintf("debian@%s:%s", remoteHost, remotePath))
- output, err := cmd.CombinedOutput()
- if err != nil {
- rps.entry.WithField("local_path", localPath).WithField("remote_path", remotePath).WithField("output", string(output)).WithError(err).Error("SCP transfer failed")
- return err
- }
- rps.entry.WithField("local_path", localPath).WithField("remote_path", remotePath).Info("Successfully transferred file")
- return nil
- }
- // validateDNSAndCertificate validates DNS resolution and Traefik certificate for debugging
- func (rps *RemotePreviewService) validateDNSAndCertificate(ctx context.Context, previewDomain, ipAddress string) {
- rps.entry.WithField("domain", previewDomain).WithField("ip_address", ipAddress).Info("Validating DNS and certificate setup")
- // Check DNS resolution from the VPS
- dnsCmd := fmt.Sprintf("nslookup %s", previewDomain)
- if err := rps.common.executeSSHCommand(ctx, ipAddress, dnsCmd); err != nil {
- rps.entry.WithField("domain", previewDomain).WithError(err).Warn("DNS resolution failed from VPS")
- }
- // Check if Traefik can see the service
- traefikCmd := "docker ps --filter 'label=traefik.enable=true' --format 'table {{.Names}}\t{{.Labels}}'"
- if err := rps.common.executeSSHCommand(ctx, ipAddress, traefikCmd); err != nil {
- rps.entry.WithError(err).Warn("Failed to check Traefik-enabled containers")
- }
- // Check Traefik logs for certificate issues
- logsCmd := "docker logs traefik --tail 20"
- if err := rps.common.executeSSHCommand(ctx, ipAddress, logsCmd); err != nil {
- rps.entry.WithError(err).Warn("Failed to get Traefik logs")
- }
- // Wait a bit for DNS propagation and certificate generation
- rps.entry.WithField("domain", previewDomain).Info("Waiting 30 seconds for DNS propagation and certificate generation")
- time.Sleep(30 * time.Second)
- }
|