worker.go
3929 bytes
1package main
2
3import (
4 "context"
5 "crypto/rand"
6 "encoding/hex"
7 "fmt"
8 "log/slog"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "strings"
13)
14
15type buildJob struct {
16 ID string
17 Request buildRequest
18 ready chan bool
19}
20
21func newBuildJob(request buildRequest) (buildJob, error) {
22 id, err := newBuildID()
23 if err != nil {
24 return buildJob{}, err
25 }
26 return buildJob{ID: id, Request: request, ready: make(chan bool, 1)}, nil
27}
28
29func newBuildID() (string, error) {
30 bytes := make([]byte, 16)
31 if _, err := rand.Read(bytes); err != nil {
32 return "", fmt.Errorf("generate build ID: %w", err)
33 }
34 return hex.EncodeToString(bytes), nil
35}
36
37func runWorker(config config, jobs <-chan buildJob, statuses *buildStatuses) {
38 for job := range jobs {
39 if !<-job.ready {
40 continue
41 }
42 request := job.Request
43 image := imageName(config.registry, request.Repository)
44 statuses.update(job, "running")
45 slog.Info("build started", "repository", request.Repository, "sha", request.SHA, "image", image)
46 if err := build(context.Background(), config, request); err != nil {
47 statuses.update(job, "failed")
48 slog.Error("build failed", "repository", request.Repository, "sha", request.SHA, "image", image, "error", err)
49 continue
50 }
51 statuses.update(job, "passed")
52 slog.Info("build completed", "repository", request.Repository, "sha", request.SHA, "image", image)
53 }
54}
55
56func build(ctx context.Context, config config, request buildRequest) error {
57 repositoryPath := filepath.Join("/soft-serve/repos", request.Repository+".git")
58 if err := run(ctx, "git", "--git-dir", repositoryPath, "rev-parse", "--verify", request.SHA+"^{commit}"); err != nil {
59 return fmt.Errorf("verify commit: %w", err)
60 }
61
62 contextPath, err := os.MkdirTemp("", "build-context-")
63 if err != nil {
64 return fmt.Errorf("create build context: %w", err)
65 }
66 defer func() {
67 if err := os.RemoveAll(contextPath); err != nil {
68 slog.Warn("remove build context", "path", contextPath, "error", err)
69 }
70 }()
71
72 if err := extractArchive(ctx, repositoryPath, request.SHA, contextPath); err != nil {
73 return err
74 }
75
76 image := imageName(config.registry, request.Repository)
77 output := fmt.Sprintf("type=image,\"name=%s\",push=true", strings.Join(imageTags(image, request), ","))
78 if err := run(
79 ctx,
80 "buildctl", "--addr", config.buildkitAddress,
81 "build", "--frontend", "dockerfile.v0",
82 "--local", "context="+contextPath,
83 "--local", "dockerfile="+contextPath,
84 "--output", output,
85 ); err != nil {
86 return fmt.Errorf("build image: %w", err)
87 }
88
89 return nil
90}
91
92func extractArchive(ctx context.Context, repositoryPath, sha, contextPath string) error {
93 archive := exec.CommandContext(ctx, "git", "--git-dir", repositoryPath, "archive", sha)
94 tar := exec.CommandContext(ctx, "tar", "-x", "-C", contextPath)
95
96 archiveOutput, err := archive.StdoutPipe()
97 if err != nil {
98 return fmt.Errorf("create archive pipe: %w", err)
99 }
100 tar.Stdin = archiveOutput
101 tar.Stdout = os.Stdout
102 tar.Stderr = os.Stderr
103
104 if err := archive.Start(); err != nil {
105 return fmt.Errorf("start archive: %w", err)
106 }
107 if err := tar.Run(); err != nil {
108 return fmt.Errorf("extract archive: %w", err)
109 }
110 if err := archive.Wait(); err != nil {
111 return fmt.Errorf("archive commit: %w", err)
112 }
113
114 return nil
115}
116
117func imageName(registry, repository string) string {
118 return registry + "/" + filepath.Base(repository)
119}
120
121func imageTags(image string, request buildRequest) []string {
122 tags := []string{image + ":" + request.SHA}
123 if request.Ref == "refs/heads/main" {
124 return append(tags, image+":main")
125 }
126 return append(tags, image+":"+strings.TrimPrefix(request.Ref, "refs/tags/"))
127}
128
129func run(ctx context.Context, name string, arguments ...string) error {
130 output, err := exec.CommandContext(ctx, name, arguments...).CombinedOutput()
131 if err == nil {
132 return nil
133 }
134
135 message := strings.TrimSpace(string(output))
136 if message == "" {
137 return err
138 }
139 return fmt.Errorf("%w: %s", err, message)
140}