e4a93fe6266c78b230b8ee6ae91ab80ff2c1b25a

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Add build worker

Diff

This diff is truncated to protect this page.

  1diff --git a/Dockerfile b/Dockerfile
  2new file mode 100644
  3index 0000000000000000000000000000000000000000..b1e6993094749a726d354e5a21b06f917cf21dc0
  4--- /dev/null
  5+++ b/Dockerfile
  6@@ -0,0 +1,22 @@
  7+FROM golang:1.27-alpine AS worker-build
  8+
  9+WORKDIR /src
 10+COPY go.mod main.go ./
 11+RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /worker .
 12+
 13+FROM moby/buildkit:rootless AS buildkit
 14+
 15+FROM alpine
 16+
 17+RUN apk add --no-cache git \
 18+    && adduser -D -u 1000 worker \
 19+    && mkdir -p /work /home/worker/.docker \
 20+    && chown -R worker:worker /work /home/worker
 21+
 22+COPY --from=buildkit /usr/bin/buildctl /usr/local/bin/buildctl
 23+COPY --from=worker-build /worker /usr/local/bin/soft-worker
 24+
 25+USER worker
 26+ENV TMPDIR=/work
 27+
 28+CMD ["soft-worker"]
 29diff --git a/go.mod b/go.mod
 30new file mode 100644
 31index 0000000000000000000000000000000000000000..ef929c55e85bd722009cac9ae288acc79fe5a8c0
 32--- /dev/null
 33+++ b/go.mod
 34@@ -0,0 +1,3 @@
 35+module soft-worker
 36+
 37+go 1.27
 38diff --git a/main.go b/main.go
 39new file mode 100644
 40index 0000000000000000000000000000000000000000..79f59a656d5a5f410f372c4dc120e2bcd40f1f77
 41--- /dev/null
 42+++ b/main.go
 43@@ -0,0 +1,214 @@
 44+package main
 45+
 46+import (
 47+	"context"
 48+	"crypto/subtle"
 49+	"encoding/json"
 50+	"errors"
 51+	"fmt"
 52+	"io"
 53+	"log"
 54+	"net/http"
 55+	"os"
 56+	"os/exec"
 57+	"path/filepath"
 58+	"regexp"
 59+)
 60+
 61+const maxRequestBodySize = 1024
 62+
 63+var commitSHA = regexp.MustCompile(`^[0-9a-f]{40}([0-9a-f]{24})?$`)
 64+
 65+type config struct {
 66+	registry        string
 67+	buildkitAddress string
 68+	token           []byte
 69+}
 70+
 71+type buildRequest struct {
 72+	Repository string `json:"repository"`
 73+	SHA        string `json:"sha"`
 74+}
 75+
 76+func main() {
 77+	config, err := loadConfig()
 78+	if err != nil {
 79+		log.Fatal(err)
 80+	}
 81+
 82+	jobs := make(chan buildRequest, 100)
 83+	go runWorker(config, jobs)
 84+
 85+	http.HandleFunc("POST /build", buildHandler(config, jobs))
 86+	log.Fatal(http.ListenAndServe(":8080", nil))
 87+}
 88+
 89+func loadConfig() (config, error) {
 90+	tokenFile, err := requiredEnvironment("CI_TOKEN_FILE")
 91+	if err != nil {
 92+		return config{}, err
 93+	}
 94+
 95+	token, err := os.ReadFile(tokenFile)
 96+	if err != nil {
 97+		return config{}, fmt.Errorf("read CI token: %w", err)
 98+	}
 99+	token = trimTrailingNewline(token)
100+	if len(token) == 0 {
101+		return config{}, errors.New("CI token is empty")
102+	}
103+
104+	registry, err := requiredEnvironment("REGISTRY")
105+	if err != nil {
106+		return config{}, err
107+	}
108+
109+	buildkitAddress, err := requiredEnvironment("BUILDKIT_ADDR")
110+	if err != nil {
111+		return config{}, err
112+	}
113+
114+	return config{
115+		registry:        registry,
116+		buildkitAddress: buildkitAddress,
117+		token:           token,
118+	}, nil
119+}
120+
121+func requiredEnvironment(name string) (string, error) {
122+	value := os.Getenv(name)
123+	if value == "" {
124+		return "", fmt.Errorf("%s is required", name)
125+	}
126+	return value, nil
127+}
128+
129+func validRepository(repository string) bool {
130+	cleaned := filepath.Clean(repository)
131+	return repository != "." && !filepath.IsAbs(repository) && cleaned == repository && cleaned != ".."
132+}
133+
134+func trimTrailingNewline(value []byte) []byte {
135+	for len(value) > 0 && (value[len(value)-1] == '\n' || value[len(value)-1] == '\r') {
136+		value = value[:len(value)-1]
137+	}
138+	return value
139+}
140+
141+func buildHandler(config config, jobs chan<- buildRequest) http.HandlerFunc {
142+	return func(writer http.ResponseWriter, request *http.Request) {