docker_build.go
96 lines1
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
}