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) }