docker_build.go

96 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
package platform

import (
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

// Upload uploads a Docker image tar.gz to the server and loads it.
// Returns the image name extracted from the file path.
func (s *Server) Upload(imagePath string) (string, error) {
	if imagePath == "" {
		return "", nil
	}

	log.Printf("   Uploading image...")

	base := filepath.Base(imagePath)
	imageName := strings.TrimSuffix(base, ".tar.gz")

	remotePath := "/tmp/" + base
	if err := s.Copy(imagePath, remotePath); err != nil {
		return "", err
	}

	log.Printf("   Loading image...")
	if _, err := s.SSHWithTimeout(5*time.Minute, "bash", "-c", fmt.Sprintf("gunzip -c %s | docker load", remotePath)); err != nil {
		return "", fmt.Errorf("docker load: %w", err)
	}

	s.SSH("rm", remotePath)

	return imageName, nil
}

// Build uploads source and builds a Docker image on the server.
// Uses git archive to capture all tracked files, avoiding brittle hardcoded lists.
func (s *Server) Build(name, source string) (string, error) {
	log.Printf("   Building on server...")

	source = strings.TrimPrefix(source, "./")
	remoteBuildDir := "/tmp/build-" + name

	if _, err := s.SSH("rm", "-rf", remoteBuildDir); err != nil {
		return "", fmt.Errorf("clean build dir: %w", err)
	}
	if _, err := s.SSH("mkdir", "-p", remoteBuildDir); err != nil {
		return "", fmt.Errorf("create build dir: %w", err)
	}

	// Warn if there are uncommitted changes (git archive only captures committed content)
	if out, err := exec.Command("git", "status", "--porcelain").Output(); err == nil && len(out) > 0 {
		log.Printf("   WARNING: uncommitted changes will NOT be included in deploy")
	}

	// Upload entire project via git archive (respects .gitignore, no build artifacts)
	log.Printf("   Uploading project...")
	tarPath := "/tmp/build-" + name + ".tar.gz"
	gitArchive := exec.Command("git", "archive", "--format=tar.gz", "-o", tarPath, "HEAD")
	if out, err := gitArchive.CombinedOutput(); err != nil {
		return "", fmt.Errorf("git archive: %s: %w", string(out), err)
	}
	defer os.Remove(tarPath)

	remoteTar := "/tmp/build-" + name + ".tar.gz"
	if err := s.Copy(tarPath, remoteTar); err != nil {
		return "", fmt.Errorf("upload archive: %w", err)
	}
	if _, err := s.SSH("tar", "-xzf", remoteTar, "-C", remoteBuildDir); err != nil {
		return "", fmt.Errorf("extract archive: %w", err)
	}
	s.SSH("rm", remoteTar)

	// Use source-specific Dockerfile if present (e.g. dns/Dockerfile)
	if source != "." {
		if srcDockerfile := filepath.Join(source, "Dockerfile"); fileExists(srcDockerfile) {
			copyCmd := fmt.Sprintf("cp %s/%s %s/Dockerfile", remoteBuildDir, srcDockerfile, remoteBuildDir)
			s.SSH("bash", "-c", copyCmd)
		}
	}

	// Build on server with BuildKit (10 minute timeout — builds can be slow)
	log.Printf("   Running docker build...")
	buildCmd := fmt.Sprintf("cd %s && DOCKER_BUILDKIT=1 docker build -t %s -f Dockerfile .", remoteBuildDir, name)
	if _, err := s.SSHWithTimeout(10*time.Minute, "bash", "-c", buildCmd); err != nil {
		return "", fmt.Errorf("remote docker build: %w", err)
	}

	s.SSH("rm", "-rf", remoteBuildDir)

	return name, nil
}