Parent directory

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}