diff options
| -rw-r--r-- | cmd/goget/main.go | 165 | ||||
| -rw-r--r-- | go.mod | 13 | ||||
| -rw-r--r-- | go.sum | 20 |
3 files changed, 198 insertions, 0 deletions
diff --git a/cmd/goget/main.go b/cmd/goget/main.go new file mode 100644 index 0000000..ae8b3a6 --- /dev/null +++ b/cmd/goget/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "slices" + "strings" + "syscall" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" + "golang.org/x/mod/semver" +) + +const ( + forgeDomain = "code.fcuny.net" + forgeUser = "fcuny" + goPkgDomain = "go.fcuny.net" +) + +var ( + goGetReqs = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "goget_requests_total", + Help: "go get requests processed, by repository name.", + }, []string{"name"}) + + modules = []string{"x"} +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + metricsMux := http.NewServeMux() + metricsMux.Handle("/metrics", promhttp.Handler()) + metricsServer := &http.Server{ + Addr: ":9091", Handler: metricsMux, + ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, + } + + go func() { + log.Println("Starting metrics server on :9091") + if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Printf("Metrics server error: %v", err) + } + }() + + s := &http.Server{ + Addr: ":8080", + Handler: handler(), + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 1 * time.Minute, + } + + go func() { + log.Println("Starting main server on :8080") + if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Printf("Main server error: %v", err) + } + }() + + <-ctx.Done() + log.Println("Shutdown signal received, starting graceful shutdown...") + + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer shutdownCancel() + + var shutdownErr error + + log.Println("Shutting down main server...") + if err := s.Shutdown(shutdownCtx); err != nil { + log.Printf("Main server shutdown error: %v", err) + shutdownErr = err + } + + log.Println("Shutting down metrics server...") + if err := metricsServer.Shutdown(shutdownCtx); err != nil { + log.Printf("Metrics server shutdown error: %v", err) + if shutdownErr == nil { + shutdownErr = err + } + } + + if shutdownErr != nil { + log.Printf("Shutdown completed with errors") + os.Exit(1) + } + + log.Println("Shutdown completed successfully") +} + +func handler() http.Handler { + mux := http.NewServeMux() + + mux.Handle(goPkgDomain+"/{name}", siteHandler(modules)) + mux.Handle(goPkgDomain+"/{name}/", siteHandler(modules)) + + goGetMux := http.NewServeMux() + for _, name := range modules { + module := goPkgDomain + "/" + name + goGetMux.Handle( + module+"/", + goImportHandler(module, "https://"+forgeDomain+"/"+forgeUser+"/"+name), + ) + } + + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + status := http.StatusMethodNotAllowed + http.Error(rw, http.StatusText(status), status) + return + } + + if r.URL.Query().Get("go-get") == "1" { + goGetMux.ServeHTTP(rw, r) + return + } + mux.ServeHTTP(rw, r) + }) +} + +func goImportHandler(module, repo string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + goGetReqs.WithLabelValues(module).Inc() + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + if _, err := fmt.Fprintf(w, `<head><meta name="go-import" content="%s git %s">`, module, repo); err != nil { + log.Printf("Error writing response: %v", err) + } + }) +} + +func siteHandler(names []string) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + name, version, hasV := strings.Cut(name, "@") + if !hasV { + name, _, _ = strings.Cut(name, ".") + } + if hasV && !semver.IsValid(version) || !slices.Contains(names, name) { + http.NotFound(rw, r) + return + } + u := &url.URL{ + Scheme: "https", + Host: forgeDomain, + Path: forgeUser + r.URL.Path, + } + if !hasV { + path, symbol, hasSymbol := strings.Cut(r.URL.Path, ".") + if hasSymbol { + u.Path = forgeUser + "/" + path + u.Fragment = symbol + } + } + http.Redirect(rw, r, u.String(), http.StatusFound) + }) +} @@ -3,3 +3,16 @@ module go.fcuny.net/x go 1.24.5 require golang.org/x/text v0.28.0 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_golang v1.23.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/sys v0.33.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect +) @@ -1,2 +1,22 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= |
