aboutsummaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
authorLena <lena@omega>2026-08-01 00:00:00 +0000
committerLena <lena@omega>2026-08-01 00:00:00 +0000
commit3284b1c4906b90ca2e3864937c66d0fa30772ebf (patch)
treead799407ac7b0a2589a344751f4c5b3cf575d5c5 /main.go
downloadgit-forge-cloudflare-3284b1c4906b90ca2e3864937c66d0fa30772ebf.tar.gz
Enter git-forge-cloudflareHEADmaster
Runs git-forge on Cloudflare Containers with R2 for durable storage. A Worker routes every request to one container through a Durable Object. Inside it, syncd mirrors the data directory to the bucket and restores it at boot, holding a write until it is durable. git-forge is neither modified nor vendored: the image builds it from its own repository at the commit named in forge.rev.
Diffstat (limited to 'main.go')
-rw-r--r--main.go160
1 files changed, 160 insertions, 0 deletions
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..2b6a983
--- /dev/null
+++ b/main.go
@@ -0,0 +1,160 @@
+package main
+
+// syncd fronts git-forge in the container. Container disk is wiped whenever
+// the instance sleeps, so the datadir is restored before git-forge starts,
+// mirrored while it runs, and flushed on the way out.
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "log"
+ "net/http"
+ "os"
+ "os/exec"
+ "os/signal"
+ "syscall"
+ "time"
+)
+
+const (
+ datadir = "/var/lib/git-forge"
+ forgeAddr = "127.0.0.1:8080"
+ listen = ":8081"
+
+ sweepInterval = 15 * time.Second
+
+ // Cloudflare's interception ca is per instance and the container is not
+ // root, so SSL_CERT_FILE names a merged copy.
+ cfCert = "/etc/cloudflare/certs/cloudflare-containers-ca.crt"
+ sysCert = "/etc/ssl/certs/ca-certificates.crt"
+ bundle = "/tmp/ca-bundle.crt"
+
+ revFile = "/etc/git-forge.rev" // written by the Containerfile from fetch-forge
+)
+
+// Package scope because fatal flushes it before exiting.
+var ship *shipper
+
+func main() {
+ // First, so a failure during startup is shipped too.
+ ship = newShipper(os.Getenv("SYNCD_LOGS"))
+ log.SetFlags(0)
+ log.SetPrefix("syncd: ")
+ log.SetOutput(ship)
+
+ endpoint := os.Getenv("SYNCD_R2")
+ if endpoint == "" {
+ fatal("SYNCD_R2 is not set, so there is nowhere durable to write")
+ }
+ // Unset, git-forge mints one into a datadir that does not survive a
+ // restart.
+ if len(os.Getenv("GIT_FORGE_COOKIE_KEY")) < 32 {
+ fatal("GIT_FORGE_COOKIE_KEY must be set to at least 32 characters, " +
+ "because this datadir does not survive a restart: wrangler secret put GIT_FORGE_COOKIE_KEY")
+ }
+
+ rev, err := os.ReadFile(revFile)
+ if err != nil {
+ rev = []byte("unknown")
+ }
+ log.Printf("git-forge %s", bytes.TrimSpace(rev))
+
+ s := newSync(newR2(endpoint), datadir)
+
+ if err := caBundle(sysCert, cfCert, bundle); err != nil {
+ // Not fatal: only outbound mail needs the intercepted ca.
+ log.Printf("no ca bundle, outbound mail over tls will fail: %v", err)
+ }
+
+ start := time.Now()
+ if err := s.restore(); err != nil {
+ // An empty datadir would be flushed over the real one.
+ fatal("restore: %v", err)
+ }
+ log.Printf("restored config and %d repository directories in %s",
+ len(s.manifest), time.Since(start).Round(time.Millisecond))
+
+ ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+ defer stop()
+
+ forge := exec.Command("git-forge")
+ // A later entry wins: git-forge has to listen where the proxy sends and
+ // write where syncd mirrors.
+ forge.Env = append(os.Environ(),
+ "SSL_CERT_FILE="+bundle,
+ "GIT_FORGE_LISTEN="+forgeAddr,
+ "GIT_FORGE_DATADIR="+datadir,
+ )
+ forge.Stdout, forge.Stderr = ship, ship
+ if err := forge.Start(); err != nil {
+ fatal("git-forge: %v", err)
+ }
+ exited := make(chan error, 1)
+ go func() { exited <- forge.Wait() }()
+
+ srv := &http.Server{
+ Addr: listen,
+ Handler: s,
+ // No read or write deadline: a push sends a whole packfile in one
+ // request and a clone streams one back.
+ ReadHeaderTimeout: 30 * time.Second,
+ IdleTimeout: 2 * time.Minute,
+ }
+ go func() {
+ if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
+ fatal("listen: %v", err)
+ }
+ }()
+ go s.sweep(ctx)
+
+ code := 0
+ select {
+ case err := <-exited:
+ // Exit nonzero and take the container down.
+ log.Printf("git-forge exited: %v", err)
+ code = 1
+ case <-ctx.Done():
+ log.Print("draining")
+ _ = forge.Process.Signal(syscall.SIGTERM)
+ <-exited
+ }
+
+ // Only meaningful now that git-forge is stopped and cannot write again.
+ shut, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ if err := srv.Shutdown(shut); err != nil {
+ log.Printf("shutdown: %v", err)
+ }
+
+ if err := s.flush(); err != nil {
+ log.Printf("final flush: %v", err)
+ code = 1
+ } else {
+ log.Print("flushed and stopping")
+ }
+ ship.close()
+ os.Exit(code)
+}
+
+// Not log.Fatal, which exits with the line still queued in the shipper.
+func fatal(format string, args ...any) {
+ log.Printf(format, args...)
+ ship.close()
+ os.Exit(1)
+}
+
+// caBundle appends the interception ca to the system roots, so oidc still
+// verifies against the real ones.
+func caBundle(sys, cf, out string) error {
+ roots, err := os.ReadFile(sys)
+ if err != nil {
+ return err
+ }
+ intercept, err := os.ReadFile(cf)
+ if err != nil {
+ // No interception configured.
+ return os.WriteFile(out, roots, 0600)
+ }
+ return os.WriteFile(out, append(append(roots, '\n'), intercept...), 0600)
+}