e4a93fe6266c78b230b8ee6ae91ab80ff2c1b25a
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
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) {