detached 2147872

feat: wire access log config and middleware into application bootstrap

commit
2147872cf3d4622aef644133c9d54e80c2eba291
author
brice · · 2026-04-03 15:21 UTC
parent
aa3dd1d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +101 -17
M internal/app/run.go +87 -3
@@ -25,6 +25,7 @@ import (
2525 "github.com/spf13/viper"
2626
2727 // Adapters - Output
28+ "github.com/bnema/gordon/internal/adapters/out/accesslog"
2829 "github.com/bnema/gordon/internal/adapters/out/docker"
2930 "github.com/bnema/gordon/internal/adapters/out/domainsecrets"
3031 "github.com/bnema/gordon/internal/adapters/out/envloader"
@@ -119,6 +120,17 @@ type Config struct {
119120 MaxBackups int `mapstructure:"max_backups"`
120121 MaxAge int `mapstructure:"max_age"`
121122 } `mapstructure:"container_logs"`
123+ AccessLog struct {
124+ Enabled bool `mapstructure:"enabled"`
125+ Format string `mapstructure:"format"`
126+ Output string `mapstructure:"output"`
127+ FilePath string `mapstructure:"file_path"`
128+ MaxSize int `mapstructure:"max_size"`
129+ MaxBackups int `mapstructure:"max_backups"`
130+ MaxAge int `mapstructure:"max_age"`
131+ ExcludeHealthChecks bool `mapstructure:"exclude_health_checks"`
132+ SyslogIdentifier string `mapstructure:"syslog_identifier"`
133+ } `mapstructure:"access_log"`
122134 } `mapstructure:"logging"`
123135
124136 Env struct {
@@ -342,6 +354,43 @@ func initLogger(cfg Config) (zerowrap.Logger, func(), error) {
342354 return log, cleanup, nil
343355 }
344356
357+// initAccessLog creates an access log writer when access logging is enabled.
358+// Returns nil, nil when disabled — callers must treat nil writer as "disabled".
359+func initAccessLog(cfg Config, log zerowrap.Logger) (*accesslog.Writer, error) {
360+ if !cfg.Logging.AccessLog.Enabled {
361+ return nil, nil
362+ }
363+
364+ filePath := cfg.Logging.AccessLog.FilePath
365+ if filePath == "" && cfg.Logging.AccessLog.Output == "file" {
366+ dataDir := cfg.Server.DataDir
367+ if dataDir == "" {
368+ dataDir = DefaultDataDir()
369+ }
370+ filePath = filepath.Join(dataDir, "logs", "access.log")
371+ }
372+
373+ writer, err := accesslog.New(accesslog.Config{
374+ Format: cfg.Logging.AccessLog.Format,
375+ Output: cfg.Logging.AccessLog.Output,
376+ FilePath: filePath,
377+ MaxSize: cfg.Logging.AccessLog.MaxSize,
378+ MaxBackups: cfg.Logging.AccessLog.MaxBackups,
379+ MaxAge: cfg.Logging.AccessLog.MaxAge,
380+ SyslogIdentifier: cfg.Logging.AccessLog.SyslogIdentifier,
381+ })
382+ if err != nil {
383+ return nil, fmt.Errorf("failed to initialize access log: %w", err)
384+ }
385+
386+ log.Info().
387+ Str("format", cfg.Logging.AccessLog.Format).
388+ Str("output", cfg.Logging.AccessLog.Output).
389+ Msg("access log enabled")
390+
391+ return writer, nil
392+}
393+
345394 // serviceInit holds the shared context for service initialization helpers.
346395 type serviceInit struct {
347396 ctx context.Context
@@ -1611,7 +1660,7 @@ func loopbackOnly(next http.Handler, log zerowrap.Logger) http.Handler {
16111660
16121661 // createHTTPHandlers creates HTTP handlers with middleware.
16131662 // Returns three handlers: registry, HTTP proxy (with CIDR + onboarding), and HTTPS proxy.
1614-func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Handler, http.Handler, http.Handler) {
1663+func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWriter middleware.AccessLogWriter) (http.Handler, http.Handler, http.Handler) {
16151664 // Parse trusted proxies once for all middleware chains.
16161665 // This ensures consistent IP extraction across logging, rate limiting, and auth.
16171666 trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies)
@@ -1703,7 +1752,18 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
17031752
17041753 httpsHandler := otelhttp.NewHandler(middleware.Chain(httpsProxyMiddlewares...)(httpsMux), "gordon.proxy.tls")
17051754
1706- return registryMux, proxyMux, httpsHandler
1755+ // Wrap top-level handlers with access logging outside all gates
1756+ // (loopbackOnly, denyAllHandler, CIDR allowlist) so every request —
1757+ // including rejected probes — produces exactly one access-log line.
1758+ var registryOut, proxyOut, httpsOut http.Handler = registryMux, proxyMux, httpsHandler
1759+ if accessWriter != nil {
1760+ excludeHC := cfg.Logging.AccessLog.ExcludeHealthChecks
1761+ registryOut = middleware.AccessLogger(accessWriter, excludeHC, log, trustedNets)(registryOut)
1762+ proxyOut = middleware.AccessLogger(accessWriter, excludeHC, log, trustedNets)(proxyOut)
1763+ httpsOut = middleware.AccessLogger(accessWriter, excludeHC, log, trustedNets)(httpsOut)
1764+ }
1765+
1766+ return registryOut, proxyOut, httpsOut
17071767 }
17081768
17091769 // directHTTPOnboardingGate returns an http.Handler that splits HTTP traffic
@@ -1984,6 +2044,21 @@ func registerAdminRoutes(registryMux *http.ServeMux, svc *services, cfg Config,
19842044 // The deferred signal.Stop calls ensure signal handlers are properly
19852045 // cleaned up before program exit, preventing signal handler leaks.
19862046 func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services, cleanupHandlers func(), log zerowrap.Logger) error {
2047+ // Initialize access log writer. Kept here (not in Run) to keep Run's cyclomatic
2048+ // complexity within the project limit of 15.
2049+ accessWriterConcrete, err := initAccessLog(cfg, log)
2050+ if err != nil {
2051+ return err
2052+ }
2053+ if accessWriterConcrete != nil {
2054+ defer accessWriterConcrete.Close()
2055+ }
2056+ // Convert to interface only when non-nil to avoid the Go nil-interface pitfall
2057+ // where a typed nil pointer becomes a non-nil interface value.
2058+ var accessWriter middleware.AccessLogWriter
2059+ if accessWriterConcrete != nil {
2060+ accessWriter = accessWriterConcrete
2061+ }
19872062 ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
19882063 defer cancel()
19892064
@@ -2001,7 +2076,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services,
20012076
20022077 errChan := make(chan error, 3)
20032078
2004- registryHandler, httpProxyHandler, httpsProxyHandler := createHTTPHandlers(svc, cfg, log)
2079+ registryHandler, httpProxyHandler, httpsProxyHandler := createHTTPHandlers(svc, cfg, log, accessWriter)
20052080
20062081 registryAddr := net.JoinHostPort(cfg.Server.RegistryListenAddr, strconv.Itoa(cfg.Server.RegistryPort))
20072082 registrySrv, registryReady := startServer(registryAddr, registryHandler, "registry", nil, errChan, log)
@@ -2702,6 +2777,15 @@ func loadConfig(v *viper.Viper, configPath string) error {
27022777 v.SetDefault("logging.container_logs.max_size", 100)
27032778 v.SetDefault("logging.container_logs.max_backups", 3)
27042779 v.SetDefault("logging.container_logs.max_age", 28)
2780+ v.SetDefault("logging.access_log.enabled", false)
2781+ v.SetDefault("logging.access_log.format", "json")
2782+ v.SetDefault("logging.access_log.output", "stdout")
2783+ v.SetDefault("logging.access_log.file_path", "")
2784+ v.SetDefault("logging.access_log.max_size", 100)
2785+ v.SetDefault("logging.access_log.max_backups", 3)
2786+ v.SetDefault("logging.access_log.max_age", 28)
2787+ v.SetDefault("logging.access_log.exclude_health_checks", true)
2788+ v.SetDefault("logging.access_log.syslog_identifier", "gordon-access")
27052789 v.SetDefault("env.dir", "") // defaults to {data_dir}/env when empty
27062790 v.SetDefault("auth.enabled", true)
27072791 // Note: auth.type defaults to "token" (the only supported mode)
@@ -25,6 +25,7 @@ import (
25 "github.com/spf13/viper"25 "github.com/spf13/viper"
26 26
27 // Adapters - Output27 // Adapters - Output
28+ "github.com/bnema/gordon/internal/adapters/out/accesslog"
28 "github.com/bnema/gordon/internal/adapters/out/docker"29 "github.com/bnema/gordon/internal/adapters/out/docker"
29 "github.com/bnema/gordon/internal/adapters/out/domainsecrets"30 "github.com/bnema/gordon/internal/adapters/out/domainsecrets"
30 "github.com/bnema/gordon/internal/adapters/out/envloader"31 "github.com/bnema/gordon/internal/adapters/out/envloader"
@@ -119,6 +120,17 @@ type Config struct {
119 MaxBackups int `mapstructure:"max_backups"`120 MaxBackups int `mapstructure:"max_backups"`
120 MaxAge int `mapstructure:"max_age"`121 MaxAge int `mapstructure:"max_age"`
121 } `mapstructure:"container_logs"`122 } `mapstructure:"container_logs"`
123+ AccessLog struct {
124+ Enabled bool `mapstructure:"enabled"`
125+ Format string `mapstructure:"format"`
126+ Output string `mapstructure:"output"`
127+ FilePath string `mapstructure:"file_path"`
128+ MaxSize int `mapstructure:"max_size"`
129+ MaxBackups int `mapstructure:"max_backups"`
130+ MaxAge int `mapstructure:"max_age"`
131+ ExcludeHealthChecks bool `mapstructure:"exclude_health_checks"`
132+ SyslogIdentifier string `mapstructure:"syslog_identifier"`
133+ } `mapstructure:"access_log"`
122 } `mapstructure:"logging"`134 } `mapstructure:"logging"`
123 135
124 Env struct {136 Env struct {
@@ -342,6 +354,43 @@ func initLogger(cfg Config) (zerowrap.Logger, func(), error) {
342 return log, cleanup, nil354 return log, cleanup, nil
343 }355 }
344 356
357+// initAccessLog creates an access log writer when access logging is enabled.
358+// Returns nil, nil when disabled — callers must treat nil writer as "disabled".
359+func initAccessLog(cfg Config, log zerowrap.Logger) (*accesslog.Writer, error) {
360+ if !cfg.Logging.AccessLog.Enabled {
361+ return nil, nil
362+ }
363+
364+ filePath := cfg.Logging.AccessLog.FilePath
365+ if filePath == "" && cfg.Logging.AccessLog.Output == "file" {
366+ dataDir := cfg.Server.DataDir
367+ if dataDir == "" {
368+ dataDir = DefaultDataDir()
369+ }
370+ filePath = filepath.Join(dataDir, "logs", "access.log")
371+ }
372+
373+ writer, err := accesslog.New(accesslog.Config{
374+ Format: cfg.Logging.AccessLog.Format,
375+ Output: cfg.Logging.AccessLog.Output,
376+ FilePath: filePath,
377+ MaxSize: cfg.Logging.AccessLog.MaxSize,
378+ MaxBackups: cfg.Logging.AccessLog.MaxBackups,
379+ MaxAge: cfg.Logging.AccessLog.MaxAge,
380+ SyslogIdentifier: cfg.Logging.AccessLog.SyslogIdentifier,
381+ })
382+ if err != nil {
383+ return nil, fmt.Errorf("failed to initialize access log: %w", err)
384+ }
385+
386+ log.Info().
387+ Str("format", cfg.Logging.AccessLog.Format).
388+ Str("output", cfg.Logging.AccessLog.Output).
389+ Msg("access log enabled")
390+
391+ return writer, nil
392+}
393+
345 // serviceInit holds the shared context for service initialization helpers.394 // serviceInit holds the shared context for service initialization helpers.
346 type serviceInit struct {395 type serviceInit struct {
347 ctx context.Context396 ctx context.Context
@@ -1611,7 +1660,7 @@ func loopbackOnly(next http.Handler, log zerowrap.Logger) http.Handler {
1611 1660
1612 // createHTTPHandlers creates HTTP handlers with middleware.1661 // createHTTPHandlers creates HTTP handlers with middleware.
1613 // Returns three handlers: registry, HTTP proxy (with CIDR + onboarding), and HTTPS proxy.1662 // Returns three handlers: registry, HTTP proxy (with CIDR + onboarding), and HTTPS proxy.
1614-func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Handler, http.Handler, http.Handler) {1663+func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWriter middleware.AccessLogWriter) (http.Handler, http.Handler, http.Handler) {
1615 // Parse trusted proxies once for all middleware chains.1664 // Parse trusted proxies once for all middleware chains.
1616 // This ensures consistent IP extraction across logging, rate limiting, and auth.1665 // This ensures consistent IP extraction across logging, rate limiting, and auth.
1617 trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies)1666 trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies)
@@ -1703,7 +1752,18 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
1703 1752
1704 httpsHandler := otelhttp.NewHandler(middleware.Chain(httpsProxyMiddlewares...)(httpsMux), "gordon.proxy.tls")1753 httpsHandler := otelhttp.NewHandler(middleware.Chain(httpsProxyMiddlewares...)(httpsMux), "gordon.proxy.tls")
1705 1754
1706- return registryMux, proxyMux, httpsHandler1755+ // Wrap top-level handlers with access logging outside all gates
1756+ // (loopbackOnly, denyAllHandler, CIDR allowlist) so every request —
1757+ // including rejected probes — produces exactly one access-log line.
1758+ var registryOut, proxyOut, httpsOut http.Handler = registryMux, proxyMux, httpsHandler
1759+ if accessWriter != nil {
1760+ excludeHC := cfg.Logging.AccessLog.ExcludeHealthChecks
1761+ registryOut = middleware.AccessLogger(accessWriter, excludeHC, log, trustedNets)(registryOut)
1762+ proxyOut = middleware.AccessLogger(accessWriter, excludeHC, log, trustedNets)(proxyOut)
1763+ httpsOut = middleware.AccessLogger(accessWriter, excludeHC, log, trustedNets)(httpsOut)
1764+ }
1765+
1766+ return registryOut, proxyOut, httpsOut
1707 }1767 }
1708 1768
1709 // directHTTPOnboardingGate returns an http.Handler that splits HTTP traffic1769 // directHTTPOnboardingGate returns an http.Handler that splits HTTP traffic
@@ -1984,6 +2044,21 @@ func registerAdminRoutes(registryMux *http.ServeMux, svc *services, cfg Config,
1984 // The deferred signal.Stop calls ensure signal handlers are properly2044 // The deferred signal.Stop calls ensure signal handlers are properly
1985 // cleaned up before program exit, preventing signal handler leaks.2045 // cleaned up before program exit, preventing signal handler leaks.
1986 func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services, cleanupHandlers func(), log zerowrap.Logger) error {2046 func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services, cleanupHandlers func(), log zerowrap.Logger) error {
2047+ // Initialize access log writer. Kept here (not in Run) to keep Run's cyclomatic
2048+ // complexity within the project limit of 15.
2049+ accessWriterConcrete, err := initAccessLog(cfg, log)
2050+ if err != nil {
2051+ return err
2052+ }
2053+ if accessWriterConcrete != nil {
2054+ defer accessWriterConcrete.Close()
2055+ }
2056+ // Convert to interface only when non-nil to avoid the Go nil-interface pitfall
2057+ // where a typed nil pointer becomes a non-nil interface value.
2058+ var accessWriter middleware.AccessLogWriter
2059+ if accessWriterConcrete != nil {
2060+ accessWriter = accessWriterConcrete
2061+ }
1987 ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)2062 ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
1988 defer cancel()2063 defer cancel()
1989 2064
@@ -2001,7 +2076,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services,
2001 2076
2002 errChan := make(chan error, 3)2077 errChan := make(chan error, 3)
2003 2078
2004- registryHandler, httpProxyHandler, httpsProxyHandler := createHTTPHandlers(svc, cfg, log)2079+ registryHandler, httpProxyHandler, httpsProxyHandler := createHTTPHandlers(svc, cfg, log, accessWriter)
2005 2080
2006 registryAddr := net.JoinHostPort(cfg.Server.RegistryListenAddr, strconv.Itoa(cfg.Server.RegistryPort))2081 registryAddr := net.JoinHostPort(cfg.Server.RegistryListenAddr, strconv.Itoa(cfg.Server.RegistryPort))
2007 registrySrv, registryReady := startServer(registryAddr, registryHandler, "registry", nil, errChan, log)2082 registrySrv, registryReady := startServer(registryAddr, registryHandler, "registry", nil, errChan, log)
@@ -2702,6 +2777,15 @@ func loadConfig(v *viper.Viper, configPath string) error {
2702 v.SetDefault("logging.container_logs.max_size", 100)2777 v.SetDefault("logging.container_logs.max_size", 100)
2703 v.SetDefault("logging.container_logs.max_backups", 3)2778 v.SetDefault("logging.container_logs.max_backups", 3)
2704 v.SetDefault("logging.container_logs.max_age", 28)2779 v.SetDefault("logging.container_logs.max_age", 28)
2780+ v.SetDefault("logging.access_log.enabled", false)
2781+ v.SetDefault("logging.access_log.format", "json")
2782+ v.SetDefault("logging.access_log.output", "stdout")
2783+ v.SetDefault("logging.access_log.file_path", "")
2784+ v.SetDefault("logging.access_log.max_size", 100)
2785+ v.SetDefault("logging.access_log.max_backups", 3)
2786+ v.SetDefault("logging.access_log.max_age", 28)
2787+ v.SetDefault("logging.access_log.exclude_health_checks", true)
2788+ v.SetDefault("logging.access_log.syslog_identifier", "gordon-access")
2705 v.SetDefault("env.dir", "") // defaults to {data_dir}/env when empty2789 v.SetDefault("env.dir", "") // defaults to {data_dir}/env when empty
2706 v.SetDefault("auth.enabled", true)2790 v.SetDefault("auth.enabled", true)
2707 // Note: auth.type defaults to "token" (the only supported mode)2791 // Note: auth.type defaults to "token" (the only supported mode)
M internal/app/run_http_handlers_test.go +14 -14
@@ -42,7 +42,7 @@ func TestCreateHTTPHandlers_LocalMode_DisablesAdminRoutes(t *testing.T) {
4242 cfg.Auth.Enabled = false
4343
4444 svc := &services{adminHandler: &adminhttp.Handler{}}
45- registryHandler, _, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())
45+ registryHandler, _, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
4646
4747 req := httptest.NewRequest(http.MethodGet, "/admin/status", nil)
4848 req.RemoteAddr = "192.0.2.10:12345"
@@ -61,7 +61,7 @@ func TestCreateHTTPHandlers_LocalMode_RestrictsRegistryToLoopback(t *testing.T)
6161 cfg := Config{}
6262 cfg.Auth.Enabled = false
6363
64- registryHandler, _, _ := createHTTPHandlers(&services{}, cfg, zerowrap.Default())
64+ registryHandler, _, _ := createHTTPHandlers(&services{}, cfg, zerowrap.Default(), nil)
6565
6666 req := httptest.NewRequest(http.MethodGet, "/v2/test/manifests/latest", nil)
6767 req.RemoteAddr = "192.0.2.20:12345"
@@ -103,7 +103,7 @@ func TestCreateHTTPHandlers_DirectHTTPRoot_ServesOnboarding(t *testing.T) {
103103 cfg := newOnboardingConfig()
104104 svc := &services{caAdapter: ca}
105105
106- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())
106+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
107107
108108 req := httptest.NewRequest(http.MethodGet, "/", nil)
109109 req.RemoteAddr = directAddr
@@ -122,7 +122,7 @@ func TestCreateHTTPHandlers_DirectHTTPCertPath_ServesCACert(t *testing.T) {
122122 cfg := newOnboardingConfig()
123123 svc := &services{caAdapter: ca}
124124
125- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())
125+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
126126
127127 req := httptest.NewRequest(http.MethodGet, "/ca.crt", nil)
128128 req.RemoteAddr = directAddr
@@ -141,7 +141,7 @@ func TestCreateHTTPHandlers_DirectHTTPUnknownPath_ReturnsForbidden(t *testing.T)
141141 cfg := newOnboardingConfig()
142142 svc := &services{caAdapter: ca}
143143
144- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())
144+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
145145
146146 req := httptest.NewRequest(http.MethodGet, "/web", nil)
147147 req.RemoteAddr = directAddr
@@ -159,7 +159,7 @@ func TestCreateHTTPHandlers_TrustedProxyHTTP_DoesNotServeOnboarding(t *testing.T
159159 cfg := newOnboardingConfig()
160160 svc := &services{caAdapter: ca}
161161
162- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())
162+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
163163
164164 req := httptest.NewRequest(http.MethodGet, "/", nil)
165165 req.RemoteAddr = trustedProxyAddr
@@ -179,7 +179,7 @@ func TestCreateHTTPHandlers_TrustedProxyCACertPath_DoesNotServeOnboardingResourc
179179 cfg := newOnboardingConfig()
180180 svc := &services{caAdapter: ca}
181181
182- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())
182+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
183183
184184 req := httptest.NewRequest(http.MethodGet, "/ca.crt", nil)
185185 req.RemoteAddr = trustedProxyAddr
@@ -199,7 +199,7 @@ func TestCreateHTTPHandlers_ForceHTTPSRedirect_DoesNotBypassDirectOnboarding(t *
199199 cfg.Server.ForceHTTPSRedirect = true
200200 svc := &services{caAdapter: ca}
201201
202- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())
202+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
203203
204204 req := httptest.NewRequest(http.MethodGet, "/", nil)
205205 req.RemoteAddr = directAddr
@@ -218,7 +218,7 @@ func TestCreateHTTPHandlers_HTTPSOnboarding_RemainsAvailable(t *testing.T) {
218218 cfg := newOnboardingConfig()
219219 svc := &services{caAdapter: ca}
220220
221- _, _, httpsHandler := createHTTPHandlers(svc, cfg, zerowrap.Default())
221+ _, _, httpsHandler := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
222222
223223 req := httptest.NewRequest(http.MethodGet, "/ca", nil)
224224 req.RemoteAddr = directAddr
@@ -236,7 +236,7 @@ func TestCreateHTTPHandlers_HTTPSOnboarding_HEADRoutesRemainAvailable(t *testing
236236 cfg := newOnboardingConfig()
237237 svc := &services{caAdapter: ca}
238238
239- _, _, httpsHandler := createHTTPHandlers(svc, cfg, zerowrap.Default())
239+ _, _, httpsHandler := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
240240
241241 // Use httptest.NewServer so Go's net/http server strips the body for HEAD.
242242 // httptest.NewRecorder does not apply server-level HEAD body stripping.
@@ -262,7 +262,7 @@ func TestCreateHTTPHandlers_DirectHTTPOnboarding_HEADRoutesRemainAvailable(t *te
262262 cfg := newOnboardingConfig()
263263 svc := &services{caAdapter: ca}
264264
265- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())
265+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
266266
267267 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
268268 r.RemoteAddr = directAddr
@@ -297,7 +297,7 @@ func TestCreateHTTPHandlers_DirectHTTPForbiddenHEAD_ReturnsForbidden(t *testing.
297297 cfg := newOnboardingConfig()
298298 svc := &services{caAdapter: ca}
299299
300- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())
300+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
301301
302302 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
303303 r.RemoteAddr = directAddr
@@ -323,7 +323,7 @@ func TestCreateHTTPHandlers_DirectHTTPACMEChallenge_Returns404(t *testing.T) {
323323 cfg := newOnboardingConfig()
324324 svc := &services{caAdapter: ca}
325325
326- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())
326+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
327327
328328 req := httptest.NewRequest(http.MethodGet, "/.well-known/acme-challenge/test-token", nil)
329329 req.RemoteAddr = directAddr
@@ -341,7 +341,7 @@ func TestCreateHTTPHandlers_TLSDisabled_DoesNotServeHTTPOnboarding(t *testing.T)
341341 cfg.Server.TLSPort = 0 // TLS disabled
342342 svc := &services{caAdapter: ca}
343343
344- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())
344+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
345345
346346 req := httptest.NewRequest(http.MethodGet, "/", nil)
347347 req.RemoteAddr = directAddr
@@ -42,7 +42,7 @@ func TestCreateHTTPHandlers_LocalMode_DisablesAdminRoutes(t *testing.T) {
42 cfg.Auth.Enabled = false42 cfg.Auth.Enabled = false
43 43
44 svc := &services{adminHandler: &adminhttp.Handler{}}44 svc := &services{adminHandler: &adminhttp.Handler{}}
45- registryHandler, _, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())45+ registryHandler, _, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
46 46
47 req := httptest.NewRequest(http.MethodGet, "/admin/status", nil)47 req := httptest.NewRequest(http.MethodGet, "/admin/status", nil)
48 req.RemoteAddr = "192.0.2.10:12345"48 req.RemoteAddr = "192.0.2.10:12345"
@@ -61,7 +61,7 @@ func TestCreateHTTPHandlers_LocalMode_RestrictsRegistryToLoopback(t *testing.T)
61 cfg := Config{}61 cfg := Config{}
62 cfg.Auth.Enabled = false62 cfg.Auth.Enabled = false
63 63
64- registryHandler, _, _ := createHTTPHandlers(&services{}, cfg, zerowrap.Default())64+ registryHandler, _, _ := createHTTPHandlers(&services{}, cfg, zerowrap.Default(), nil)
65 65
66 req := httptest.NewRequest(http.MethodGet, "/v2/test/manifests/latest", nil)66 req := httptest.NewRequest(http.MethodGet, "/v2/test/manifests/latest", nil)
67 req.RemoteAddr = "192.0.2.20:12345"67 req.RemoteAddr = "192.0.2.20:12345"
@@ -103,7 +103,7 @@ func TestCreateHTTPHandlers_DirectHTTPRoot_ServesOnboarding(t *testing.T) {
103 cfg := newOnboardingConfig()103 cfg := newOnboardingConfig()
104 svc := &services{caAdapter: ca}104 svc := &services{caAdapter: ca}
105 105
106- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())106+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
107 107
108 req := httptest.NewRequest(http.MethodGet, "/", nil)108 req := httptest.NewRequest(http.MethodGet, "/", nil)
109 req.RemoteAddr = directAddr109 req.RemoteAddr = directAddr
@@ -122,7 +122,7 @@ func TestCreateHTTPHandlers_DirectHTTPCertPath_ServesCACert(t *testing.T) {
122 cfg := newOnboardingConfig()122 cfg := newOnboardingConfig()
123 svc := &services{caAdapter: ca}123 svc := &services{caAdapter: ca}
124 124
125- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())125+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
126 126
127 req := httptest.NewRequest(http.MethodGet, "/ca.crt", nil)127 req := httptest.NewRequest(http.MethodGet, "/ca.crt", nil)
128 req.RemoteAddr = directAddr128 req.RemoteAddr = directAddr
@@ -141,7 +141,7 @@ func TestCreateHTTPHandlers_DirectHTTPUnknownPath_ReturnsForbidden(t *testing.T)
141 cfg := newOnboardingConfig()141 cfg := newOnboardingConfig()
142 svc := &services{caAdapter: ca}142 svc := &services{caAdapter: ca}
143 143
144- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())144+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
145 145
146 req := httptest.NewRequest(http.MethodGet, "/web", nil)146 req := httptest.NewRequest(http.MethodGet, "/web", nil)
147 req.RemoteAddr = directAddr147 req.RemoteAddr = directAddr
@@ -159,7 +159,7 @@ func TestCreateHTTPHandlers_TrustedProxyHTTP_DoesNotServeOnboarding(t *testing.T
159 cfg := newOnboardingConfig()159 cfg := newOnboardingConfig()
160 svc := &services{caAdapter: ca}160 svc := &services{caAdapter: ca}
161 161
162- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())162+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
163 163
164 req := httptest.NewRequest(http.MethodGet, "/", nil)164 req := httptest.NewRequest(http.MethodGet, "/", nil)
165 req.RemoteAddr = trustedProxyAddr165 req.RemoteAddr = trustedProxyAddr
@@ -179,7 +179,7 @@ func TestCreateHTTPHandlers_TrustedProxyCACertPath_DoesNotServeOnboardingResourc
179 cfg := newOnboardingConfig()179 cfg := newOnboardingConfig()
180 svc := &services{caAdapter: ca}180 svc := &services{caAdapter: ca}
181 181
182- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())182+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
183 183
184 req := httptest.NewRequest(http.MethodGet, "/ca.crt", nil)184 req := httptest.NewRequest(http.MethodGet, "/ca.crt", nil)
185 req.RemoteAddr = trustedProxyAddr185 req.RemoteAddr = trustedProxyAddr
@@ -199,7 +199,7 @@ func TestCreateHTTPHandlers_ForceHTTPSRedirect_DoesNotBypassDirectOnboarding(t *
199 cfg.Server.ForceHTTPSRedirect = true199 cfg.Server.ForceHTTPSRedirect = true
200 svc := &services{caAdapter: ca}200 svc := &services{caAdapter: ca}
201 201
202- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())202+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
203 203
204 req := httptest.NewRequest(http.MethodGet, "/", nil)204 req := httptest.NewRequest(http.MethodGet, "/", nil)
205 req.RemoteAddr = directAddr205 req.RemoteAddr = directAddr
@@ -218,7 +218,7 @@ func TestCreateHTTPHandlers_HTTPSOnboarding_RemainsAvailable(t *testing.T) {
218 cfg := newOnboardingConfig()218 cfg := newOnboardingConfig()
219 svc := &services{caAdapter: ca}219 svc := &services{caAdapter: ca}
220 220
221- _, _, httpsHandler := createHTTPHandlers(svc, cfg, zerowrap.Default())221+ _, _, httpsHandler := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
222 222
223 req := httptest.NewRequest(http.MethodGet, "/ca", nil)223 req := httptest.NewRequest(http.MethodGet, "/ca", nil)
224 req.RemoteAddr = directAddr224 req.RemoteAddr = directAddr
@@ -236,7 +236,7 @@ func TestCreateHTTPHandlers_HTTPSOnboarding_HEADRoutesRemainAvailable(t *testing
236 cfg := newOnboardingConfig()236 cfg := newOnboardingConfig()
237 svc := &services{caAdapter: ca}237 svc := &services{caAdapter: ca}
238 238
239- _, _, httpsHandler := createHTTPHandlers(svc, cfg, zerowrap.Default())239+ _, _, httpsHandler := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
240 240
241 // Use httptest.NewServer so Go's net/http server strips the body for HEAD.241 // Use httptest.NewServer so Go's net/http server strips the body for HEAD.
242 // httptest.NewRecorder does not apply server-level HEAD body stripping.242 // httptest.NewRecorder does not apply server-level HEAD body stripping.
@@ -262,7 +262,7 @@ func TestCreateHTTPHandlers_DirectHTTPOnboarding_HEADRoutesRemainAvailable(t *te
262 cfg := newOnboardingConfig()262 cfg := newOnboardingConfig()
263 svc := &services{caAdapter: ca}263 svc := &services{caAdapter: ca}
264 264
265- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())265+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
266 266
267 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {267 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
268 r.RemoteAddr = directAddr268 r.RemoteAddr = directAddr
@@ -297,7 +297,7 @@ func TestCreateHTTPHandlers_DirectHTTPForbiddenHEAD_ReturnsForbidden(t *testing.
297 cfg := newOnboardingConfig()297 cfg := newOnboardingConfig()
298 svc := &services{caAdapter: ca}298 svc := &services{caAdapter: ca}
299 299
300- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())300+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
301 301
302 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {302 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
303 r.RemoteAddr = directAddr303 r.RemoteAddr = directAddr
@@ -323,7 +323,7 @@ func TestCreateHTTPHandlers_DirectHTTPACMEChallenge_Returns404(t *testing.T) {
323 cfg := newOnboardingConfig()323 cfg := newOnboardingConfig()
324 svc := &services{caAdapter: ca}324 svc := &services{caAdapter: ca}
325 325
326- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())326+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
327 327
328 req := httptest.NewRequest(http.MethodGet, "/.well-known/acme-challenge/test-token", nil)328 req := httptest.NewRequest(http.MethodGet, "/.well-known/acme-challenge/test-token", nil)
329 req.RemoteAddr = directAddr329 req.RemoteAddr = directAddr
@@ -341,7 +341,7 @@ func TestCreateHTTPHandlers_TLSDisabled_DoesNotServeHTTPOnboarding(t *testing.T)
341 cfg.Server.TLSPort = 0 // TLS disabled341 cfg.Server.TLSPort = 0 // TLS disabled
342 svc := &services{caAdapter: ca}342 svc := &services{caAdapter: ca}
343 343
344- _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default())344+ _, httpHandler, _ := createHTTPHandlers(svc, cfg, zerowrap.Default(), nil)
345 345
346 req := httptest.NewRequest(http.MethodGet, "/", nil)346 req := httptest.NewRequest(http.MethodGet, "/", nil)
347 req.RemoteAddr = directAddr347 req.RemoteAddr = directAddr

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close