v3.3.1 31f1eec

services.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"fmt"
6	"net/http"
7	"path/filepath"
8	"time"
9
10	"github.com/bnema/zerowrap"
11	"github.com/spf13/viper"
12
13	"github.com/bnema/gordon/internal/adapters/in/http/admin"
14	authhandler "github.com/bnema/gordon/internal/adapters/in/http/auth"
15	"github.com/bnema/gordon/internal/adapters/in/http/registry"
16	trafficadapter "github.com/bnema/gordon/internal/adapters/in/traffic"
17	acmelego "github.com/bnema/gordon/internal/adapters/out/acmelego"
18	acmestore "github.com/bnema/gordon/internal/adapters/out/acmestore"
19	"github.com/bnema/gordon/internal/adapters/out/docker"
20	"github.com/bnema/gordon/internal/adapters/out/eventbus"
21	"github.com/bnema/gordon/internal/adapters/out/filesystem"
22	"github.com/bnema/gordon/internal/adapters/out/logwriter"
23	pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"
24	"github.com/bnema/gordon/internal/adapters/out/secrets"
25	"github.com/bnema/gordon/internal/adapters/out/telemetry"
26	"github.com/bnema/gordon/internal/boundaries/in"
27	"github.com/bnema/gordon/internal/boundaries/out"
28	"github.com/bnema/gordon/internal/domain"
29	"github.com/bnema/gordon/internal/usecase/apps"
30	"github.com/bnema/gordon/internal/usecase/apptraffic"
31	"github.com/bnema/gordon/internal/usecase/auth"
32	"github.com/bnema/gordon/internal/usecase/backup"
33	"github.com/bnema/gordon/internal/usecase/config"
34	"github.com/bnema/gordon/internal/usecase/container"
35	"github.com/bnema/gordon/internal/usecase/deployment"
36	"github.com/bnema/gordon/internal/usecase/health"
37	"github.com/bnema/gordon/internal/usecase/images"
38	"github.com/bnema/gordon/internal/usecase/logs"
39	pkiusecase "github.com/bnema/gordon/internal/usecase/pki"
40	"github.com/bnema/gordon/internal/usecase/proxy"
41	"github.com/bnema/gordon/internal/usecase/publictls"
42	registrySvc "github.com/bnema/gordon/internal/usecase/registry"
43	"github.com/bnema/gordon/internal/usecase/registrystate"
44	volumesSvc "github.com/bnema/gordon/internal/usecase/volumes"
45	"github.com/bnema/gordon/pkg/bytesize"
46)
47
48// services holds all the services used by the application.
49type services struct {
50	runtime               *docker.Runtime
51	eventBus              *eventbus.InMemory
52	metrics               *telemetry.Metrics
53	blobStorage           *filesystem.BlobStorage
54	manifestStorage       *filesystem.ManifestStorage
55	backupStorage         *filesystem.BackupStorage
56	volumeBackupStore     out.VolumeBackupStorage
57	volumeBackupCfg       domain.VolumeBackupConfig
58	logWriter             *logwriter.LogWriter
59	tokenStore            out.TokenStore
60	configSvc             *config.Service
61	containerSvc          *container.Service
62	backupSvc             *backup.Service
63	volumeBackupSvc       *backup.VolumeService
64	registrySvc           *registrySvc.Service
65	healthSvc             *health.Service
66	logSvc                *logs.Service
67	imageSvc              *images.Service
68	volumeSvc             *volumesSvc.Service
69	proxySvc              *proxy.Service
70	serviceSecretProvider out.SecretProvider
71	authSvc               *auth.Service
72	authHandler           *authhandler.Handler
73	adminHandler          *admin.Handler
74	httpProxyHandler      http.Handler
75	httpsProxyHandler     http.Handler
76	internalRegUser       string
77	internalRegPass       string
78	maxBlobChunkSize      int64
79	maxBlobSize           int64
80	caAdapter             *pkiadapter.CA
81	pkiSvc                *pkiusecase.Service
82	appState              out.AppState
83	// gcBarrier serializes resource acquisition and its durable
84	// protection publication against destructive prune.
85	gcBarrier            out.GCBarrier
86	appDeploySvc         *deployment.Service
87	appSvc               in.AppService
88	appSvcImpl           *apps.AppServiceImpl
89	appActivator         *apptraffic.Activator
90	appHostIndex         *apptraffic.HostIndex
91	appTrafficPublisher  *appTrafficPublisher
92	appMonitor           *appMonitor
93	reloadCoordinator    *reloadCoordinator
94	publicTLSSvc         in.PublicTLSService
95	publicTLSRuntime     publicTLSRuntime
96	trafficManager       *trafficadapter.Manager
97	tlsHTTPEntryPoints   map[string]struct{}
98	smartHTTPEntryPoints map[string]struct{}
99	registryHandler      interface {
100		UpdateBlobLimits(maxBlobChunkSize, maxBlobSize int64)
101	}
102	// workloadLogs exports proxy access and app container logs over
103	// OTLP. Nil when telemetry log export is disabled.
104	workloadLogs out.LogExporter
105}
106
107// serviceInit holds the shared context for service initialization helpers.
108type serviceInit struct {
109	ctx context.Context
110	v   *viper.Viper
111	cfg Config
112	log zerowrap.Logger
113	svc *services
114}
115
116// createServices creates all the application services for server runtime.
117// Public ACME reconciliation is started later, after the HTTP listener is bound.
118func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowrap.Logger) (_ *services, retErr error) {
119	return createServicesWithOptions(ctx, v, cfg, log)
120}
121
122// createServicesWithOptions creates all the application services.
123// ACME Reconcile and renewal loop are started later from runServers,
124// after HTTP listeners are bound.
125func createServicesWithOptions(ctx context.Context, v *viper.Viper, cfg Config, log zerowrap.Logger) (_ *services, retErr error) {
126	si := &serviceInit{
127		ctx: ctx,
128		v:   v,
129		cfg: cfg,
130		log: log,
131		svc: &services{},
132	}
133	defer func() {
134		if retErr != nil {
135			if si.svc.pkiSvc != nil {
136				si.svc.pkiSvc.Stop()
137			}
138			if si.svc.publicTLSSvc != nil {
139				ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
140				defer cancel()
141				if err := si.svc.publicTLSSvc.Stop(ctx); err != nil {
142					si.log.Warn().Err(err).Msg("failed to stop public TLS service during createServices cleanup")
143				}
144			}
145		}
146	}()
147	var err error
148
149	// Create output adapters
150	runtimeSocket := resolveRuntimeConfig(v.GetString("server.runtime"))
151	if si.svc.runtime, si.svc.eventBus, err = createOutputAdapters(ctx, log, runtimeSocket); err != nil {
152		return nil, err
153	}
154
155	// Create storage
156	if si.svc.blobStorage, si.svc.manifestStorage, err = createStorage(cfg, log); err != nil {
157		return nil, err
158	}
159
160	// Create log writer
161	if si.svc.logWriter, err = createLogWriter(cfg, log); err != nil {
162		return nil, err
163	}
164
165	// Create auth service (if enabled)
166	if si.svc.tokenStore, si.svc.authSvc, err = createAuthService(ctx, cfg, log); err != nil {
167		return nil, err
168	}
169
170	if err := setupInternalRegistryAuth(si.svc, log); err != nil {
171		return nil, err
172	}
173
174	// Create config service
175	si.svc.configSvc = config.NewService(v, si.svc.eventBus)
176	if err := si.svc.configSvc.Load(ctx); err != nil {
177		return nil, log.WrapErr(err, "failed to load configuration")
178	}
179
180	if err := si.initPKI(); err != nil {
181		return nil, err
182	}
183
184	if err := si.initSecrets(); err != nil {
185		return nil, err
186	}
187
188	if err := si.initPublicTLS(); err != nil {
189		return nil, err
190	}
191
192	if err := si.initRuntimeProxyAndTraffic(); err != nil {
193		return nil, err
194	}
195
196	runtime := &reloadRuntime{v: v, svc: si.svc, log: log}
197	si.svc.reloadCoordinator = newReloadCoordinator(v, si.svc.configSvc, si.svc.proxySvc, nil, si.svc.eventBus, si.svc.publicTLSSvc, runtime.Apply, log)
198
199	si.initHandlers()
200
201	return si.svc, nil
202}
203
204// initPKI initialises the internal CA and PKI service when TLS is enabled.
205func (si *serviceInit) initPKI() error {
206	if !hasTLSCapableEntrypoint(si.cfg) {
207		si.log.Info().Msg("internal CA disabled (no TLS-capable entrypoint configured)")
208		return nil
209	}
210
211	if (si.cfg.Server.TLSCertFile == "") != (si.cfg.Server.TLSKeyFile == "") {
212		return fmt.Errorf("both tls_cert_file and tls_key_file must be set, or neither")
213	}
214
215	caAdapter, err := pkiadapter.NewCA(resolveDataDir(si.cfg.Server.DataDir), si.log)
216	if err != nil {
217		return si.log.WrapErr(err, "failed to initialize internal CA")
218	}
219	si.svc.caAdapter = caAdapter
220	si.svc.pkiSvc = pkiusecase.NewService(si.ctx, caAdapter, si.appRoutes(), []string{si.cfg.Server.GordonDomain}, si.log)
221	return nil
222}
223
224// initPublicTLS initializes the public ACME TLS service if enabled.
225func (si *serviceInit) initPublicTLS() error {
226	if !si.cfg.TLS.ACME.Enabled {
227		return nil
228	}
229
230	if err := validatePublicTLSReadiness(si.cfg); err != nil {
231		return err
232	}
233
234	ctx := si.ctx
235	log := si.log
236
237	dnsCfg, err := buildDNSConfig(si.cfg)
238	if err != nil {
239		return log.WrapErr(err, "invalid DNS configuration")
240	}
241
242	publicTLSCfg := publictls.Config{
243		Enabled:         si.cfg.TLS.ACME.Enabled,
244		Email:           si.cfg.TLS.ACME.Email,
245		Challenge:       si.cfg.TLS.ACME.Challenge,
246		HTTPPort:        effectiveHTTP01Port(si.cfg),
247		TLSPort:         effectivePublicTLSPort(si.cfg),
248		DataDir:         resolveDataDir(si.cfg.Server.DataDir),
249		ObtainBatchSize: si.cfg.TLS.ACME.ObtainBatchSize,
250		DNS:             dnsCfg,
251	}
252
253	tokenResolver := secrets.NewPublicTLSResolver(secrets.PublicTLSResolverConfig{})
254
255	effective, err := publictls.ResolveEffectiveChallenge(ctx, publicTLSCfg, tokenResolver)
256	if err != nil {
257		return log.WrapErr(err, "resolve ACME challenge")
258	}
259	if err := validateEffectivePublicTLSReadiness(si.cfg, effective); err != nil {
260		return err
261	}
262
263	store, err := acmestore.New(filepath.Join(resolveDataDir(si.cfg.Server.DataDir), "acme"))
264	if err != nil {
265		return log.WrapErr(err, "create ACME store")
266	}
267
268	challenges := publictls.NewHTTP01Challenges()
269
270	var zoneResolver *acmelego.CloudflareZoneResolver
271	if effective.Mode == domain.ACMEChallengeCloudflareDNS01 {
272		zoneResolver = acmelego.NewCloudflareZoneResolver(effective.Token)
273	}
274
275	issuer, err := acmelego.NewIssuer(acmelego.Config{
276		Email:                 si.cfg.TLS.ACME.Email,
277		Challenge:             effective.Mode,
278		Token:                 effective.Token,
279		Store:                 store,
280		HTTPChallengeSink:     challenges,
281		DNSResolvers:          publicTLSCfg.DNS.Resolvers,
282		DNSPropagationTimeout: publicTLSCfg.DNS.PropagationTimeout,
283		DNSPollingInterval:    publicTLSCfg.DNS.PollingInterval,
284	})
285	if err != nil {
286		return log.WrapErr(err, "create ACME issuer")
287	}
288
289	svc := publictls.NewService(publicTLSCfg, publictls.ServiceDeps{
290		Routes:          si.appRoutes(),
291		Issuer:          issuer,
292		Store:           store,
293		ZoneResolver:    zoneResolver,
294		Challenges:      challenges,
295		Effective:       effective,
296		AdditionalHosts: []string{si.cfg.Server.GordonDomain},
297	})
298
299	if err := svc.Load(ctx); err != nil {
300		log.Warn().Err(err).Msg("failed to load ACME certificates, continuing")
301	}
302
303	log.Info().
304		Str("email", si.cfg.TLS.ACME.Email).
305		Str("challenge", string(effective.Mode)).
306		Msg("public ACME TLS initialized (runtime start deferred)")
307
308	si.svc.publicTLSSvc = svc
309	si.svc.publicTLSRuntime = svc
310	return nil
311}
312
313// publicTLSRuntime is the subset of in.PublicTLSService needed at server runtime
314// after the HTTP listener is bound: reconcile missing certs and start the renewal
315// loop. It is nil when ACME is disabled.
316type publicTLSRuntime interface {
317	Reconcile(context.Context) error
318	StartRenewalLoop(context.Context, time.Duration) <-chan struct{}
319}
320
321func startPublicTLSRuntime(ctx context.Context, svc publicTLSRuntime, log zerowrap.Logger) error {
322	if svc == nil {
323		return nil
324	}
325	reconcileErr := svc.Reconcile(ctx)
326	svc.StartRenewalLoop(ctx, time.Hour)
327	log.Info().Msg("public ACME TLS runtime started")
328	return reconcileErr
329}
330
331func startPublicTLSRuntimeWithWarning(ctx context.Context, svc publicTLSRuntime, log zerowrap.Logger) {
332	if err := startPublicTLSRuntime(ctx, svc, log); err != nil {
333		log.Warn().Err(err).Msg("initial public ACME reconcile failed, continuing with renewal loop")
334	}
335}
336
337// initSecrets validates the secrets backend and creates the app service
338// secret provider.
339func (si *serviceInit) initSecrets() error {
340	backend, err := resolveSecretsBackend(si.cfg.Auth.SecretsBackend)
341	if err != nil {
342		return si.log.WrapErr(err, "failed to resolve secrets backend")
343	}
344	si.svc.serviceSecretProvider = createStandaloneServiceSecretProvider(backend, resolveDataDir(si.cfg.Server.DataDir), si.log)
345	return nil
346}
347
348// initRuntimeAndProxy creates container, backup, registry, image, volume, and proxy services.
349func (si *serviceInit) initRuntimeProxyAndTraffic() error {
350	if err := si.initRuntimeAndProxy(); err != nil {
351		return err
352	}
353	si.svc.trafficManager = trafficadapter.NewManager()
354	return si.initApps()
355}
356
357func containerResourceLimits(cfg Config) (deployment.ResourceLimits, error) {
358	limits := deployment.ResourceLimits{PidsLimit: cfg.Containers.PidsLimit}
359	if cfg.Containers.MemoryLimit != "" {
360		parsed, err := bytesize.Parse(cfg.Containers.MemoryLimit)
361		if err != nil {
362			return limits, fmt.Errorf("invalid containers.memory_limit %q: %w", cfg.Containers.MemoryLimit, err)
363		}
364		limits.MemoryBytes = parsed
365	}
366	if cfg.Containers.CPULimit > 0 {
367		limits.NanoCPUs = int64(cfg.Containers.CPULimit * float64(time.Second))
368	}
369	return limits, nil
370}
371
372func (si *serviceInit) initRuntimeAndProxy() error {
373	var err error
374
375	if si.svc.containerSvc, err = createContainerService(si.ctx, si.v, si.cfg, si.svc, si.log); err != nil {
376		return err
377	}
378
379	if si.svc.backupStorage, si.svc.backupSvc, err = createBackupService(si.cfg, si.svc, si.log); err != nil {
380		return err
381	}
382	if si.svc.volumeBackupStore, si.svc.volumeBackupSvc, si.svc.volumeBackupCfg, err = createVolumeBackupService(si.ctx, si.cfg, si.svc, si.log); err != nil {
383		return err
384	}
385
386	registryState := registrystate.New()
387	si.svc.registrySvc = registrySvc.NewService(si.svc.blobStorage, si.svc.manifestStorage, si.svc.eventBus, registryState)
388	si.svc.imageSvc = images.NewService(si.svc.runtime, si.svc.manifestStorage, si.svc.blobStorage, si.log, registryState)
389	si.svc.volumeSvc = volumesSvc.NewService(si.svc.runtime)
390
391	injectTelemetryMetrics(si.cfg, si.svc, si.log)
392
393	proxyCfg, err := buildProxyConfig(si.cfg, si.log)
394	if err != nil {
395		return err
396	}
397	si.svc.maxBlobChunkSize = proxyCfg.maxBlobChunkSize
398	si.svc.maxBlobSize = proxyCfg.maxBlobSize
399	si.svc.proxySvc = proxy.NewService(si.svc.configSvc, proxyCfg.proxyConfig)
400
401	// Wire synchronous proxy cache invalidation for zero-downtime deployments.
402	return nil
403}
404
405func (si *serviceInit) initHandlers() {
406	if si.svc.authSvc != nil {
407		internalAuth := authhandler.InternalAuth{
408			Username: si.svc.internalRegUser,
409			Password: si.svc.internalRegPass,
410		}
411		si.svc.authHandler = authhandler.NewHandler(si.svc.authSvc, internalAuth, si.log)
412	}
413
414	si.svc.logSvc = logs.NewService(resolveLogFilePath(si.cfg), si.cfg.Logging.File.Enabled, si.svc.runtime, si.log)
415	// initApps (via initRuntimeProxyAndTraffic) runs before initHandlers,
416	// so durable ACTIVE state is already open here.
417	if si.svc.appState != nil {
418		si.svc.logSvc.WithAppState(si.svc.appState)
419	}
420
421	if si.svc.trafficManager == nil {
422		si.svc.trafficManager = trafficadapter.NewManager()
423	}
424
425	si.svc.adminHandler = admin.NewHandler(admin.HandlerDeps{
426		ConfigSvc:       si.svc.configSvc,
427		AuthSvc:         si.svc.authSvc,
428		ContainerSvc:    si.svc.containerSvc,
429		HealthSvc:       si.svc.healthSvc,
430		LogSvc:          si.svc.logSvc,
431		RegistrySvc:     si.svc.registrySvc,
432		ReloadTrigger:   si.svc.reloadCoordinator,
433		Log:             si.log,
434		BackupSvc:       si.svc.backupSvc,
435		VolumeBackupSvc: si.svc.volumeBackupSvc,
436		ImageSvc:        si.svc.imageSvc,
437		VolumeSvc:       si.svc.volumeSvc,
438		PublicTLSSvc:    si.svc.publicTLSSvc,
439		TrafficSvc:      si.svc.trafficManager,
440		AppSvc:          si.svc.appSvc,
441		CASvc:           si.caInfoProvider(),
442	})
443}
444
445// caInfoProvider returns the internal CA as a CAInfoService, or an untyped nil
446// when internal TLS is disabled so the admin handler sees a nil interface.
447func (si *serviceInit) caInfoProvider() in.CAInfoService {
448	if si.svc.caAdapter == nil {
449		return nil
450	}
451	return si.svc.caAdapter
452}
453
454// injectTelemetryMetrics creates and injects OTel metrics into services when
455// telemetry is enabled. Skipped otherwise to avoid unnecessary allocations.
456func injectTelemetryMetrics(cfg Config, svc *services, log zerowrap.Logger) {
457	if !cfg.Telemetry.Enabled || !cfg.Telemetry.Metrics {
458		return
459	}
460	gordonMetrics, err := telemetry.NewMetrics()
461	if err != nil {
462		log.Warn().Err(err).Msg("failed to create telemetry metrics, continuing without metrics")
463		return
464	}
465	svc.registrySvc.SetMetrics(gordonMetrics)
466	svc.eventBus.SetMetrics(gordonMetrics)
467	svc.metrics = gordonMetrics
468}
469
470// observeManagedContainers exports the managed-container gauge from app
471// state. No-op when metrics are disabled.
472func observeManagedContainers(metrics *telemetry.Metrics, state out.AppStateReader, log zerowrap.Logger) {
473	if metrics == nil {
474		return
475	}
476	if err := metrics.ObserveManagedContainers(func(ctx context.Context) (int64, error) {
477		return apps.CountManagedContainers(ctx, state)
478	}); err != nil {
479		log.Warn().Err(err).Msg("failed to register managed container gauge")
480	}
481}
482
483// createOutputAdapters creates the container runtime and event bus.
484func createOutputAdapters(ctx context.Context, log zerowrap.Logger, runtimeSocket string) (*docker.Runtime, *eventbus.InMemory, error) {
485	detection := docker.DetectRuntimeSocket(runtimeSocket)
486
487	var runtime *docker.Runtime
488	var err error
489
490	switch detection.Source {
491	case "none":
492		return nil, nil, fmt.Errorf("no container runtime found: checked Docker socket, Podman socket, DOCKER_HOST env var. Install Docker or Podman, or set server.runtime in config")
493	case "DOCKER_HOST_passthrough":
494		detection.RuntimeName = "docker"
495		runtime, err = docker.NewRuntime()
496	default:
497		if detection.SocketPath != "" {
498			runtime, err = docker.NewRuntimeWithSocket(detection.SocketPath)
499		} else {
500			runtime, err = docker.NewRuntime()
501		}
502	}
503	if err != nil {
504		return nil, nil, log.WrapErr(err, "failed to create container runtime")
505	}
506
507	if err := runtime.Ping(ctx); err != nil {
508		return nil, nil, log.WrapErr(err, fmt.Sprintf("container runtime not available (detected: %s via %s)", detection.RuntimeName, detection.Source))
509	}
510
511	runtimeVersion, _ := runtime.Version(ctx)
512	log.Info().
513		Str("runtime", detection.RuntimeName).
514		Str("version", runtimeVersion).
515		Str("source", detection.Source).
516		Msg("container runtime initialized")
517
518	eventBus := eventbus.NewInMemory(100, log)
519
520	return runtime, eventBus, nil
521}
522
523// createStorage creates blob and manifest storage.
524func createStorage(cfg Config, log zerowrap.Logger) (*filesystem.BlobStorage, *filesystem.ManifestStorage, error) {
525	dataDir := cfg.Server.DataDir
526	if dataDir == "" {
527		dataDir = DefaultDataDir()
528	}
529
530	registryDir := filepath.Join(dataDir, "registry")
531
532	blobStorage, err := filesystem.NewBlobStorage(registryDir, log)
533	if err != nil {
534		return nil, nil, log.WrapErr(err, "failed to create blob storage")
535	}
536
537	manifestStorage, err := filesystem.NewManifestStorage(registryDir, log)
538	if err != nil {
539		return nil, nil, log.WrapErr(err, "failed to create manifest storage")
540	}
541
542	return blobStorage, manifestStorage, nil
543}
544
545// createLogWriter creates the container log writer.
546func createLogWriter(cfg Config, log zerowrap.Logger) (*logwriter.LogWriter, error) {
547	if !cfg.Logging.ContainerLogs.Enabled {
548		log.Debug().Msg("container log collection disabled")
549		return nil, nil
550	}
551
552	// Determine log directory
553	logDir := cfg.Logging.ContainerLogs.Dir
554	if logDir == "" {
555		dataDir := cfg.Server.DataDir
556		if dataDir == "" {
557			dataDir = DefaultDataDir()
558		}
559		logDir = filepath.Join(dataDir, "logs", "containers")
560	}
561
562	writer, err := logwriter.New(logwriter.Config{
563		Dir:        logDir,
564		MaxSize:    cfg.Logging.ContainerLogs.MaxSize,
565		MaxBackups: cfg.Logging.ContainerLogs.MaxBackups,
566		MaxAge:     cfg.Logging.ContainerLogs.MaxAge,
567	})
568	if err != nil {
569		return nil, log.WrapErr(err, "failed to create container log writer")
570	}
571
572	log.Info().Str("dir", logDir).Msg("container log collection enabled")
573	return writer, nil
574}
575
576// proxyConfigResult holds parsed proxy and blob chunk size config.
577type proxyConfigResult struct {
578	proxyConfig      proxy.Config
579	maxBlobChunkSize int64
580	maxBlobSize      int64
581}
582
583// buildProxyConfig parses size-related config fields and builds the proxy config.
584func buildProxyConfig(cfg Config, log zerowrap.Logger) (*proxyConfigResult, error) {
585	maxProxyBodySize := int64(512 << 20) // 512MB default
586	if cfg.Server.MaxProxyBodySize != "" {
587		parsedSize, err := bytesize.Parse(cfg.Server.MaxProxyBodySize)
588		if err != nil {
589			return nil, log.WrapErrWithFields(err, "invalid server.max_proxy_body_size configuration", map[string]any{"value": cfg.Server.MaxProxyBodySize})
590		}
591		maxProxyBodySize = parsedSize
592	}
593
594	maxBlobChunkSize := int64(registry.DefaultMaxBlobChunkSize)
595	if cfg.Server.MaxBlobChunkSize != "" {
596		parsedSize, err := bytesize.Parse(cfg.Server.MaxBlobChunkSize)
597		if err != nil {
598			return nil, log.WrapErrWithFields(err, "invalid server.max_blob_chunk_size configuration", map[string]any{"value": cfg.Server.MaxBlobChunkSize})
599		}
600		maxBlobChunkSize = parsedSize
601	}
602
603	maxBlobSize := int64(registry.DefaultMaxBlobSize)
604	if cfg.Server.MaxBlobSize != "" {
605		parsedSize, err := bytesize.Parse(cfg.Server.MaxBlobSize)
606		if err != nil {
607			return nil, log.WrapErrWithFields(err, "invalid server.max_blob_size configuration", map[string]any{"value": cfg.Server.MaxBlobSize})
608		}
609		maxBlobSize = parsedSize
610	}
611
612	maxProxyResponseSize := int64(1 << 30) // 1GB default
613	if cfg.Server.MaxProxyResponseSize != "" {
614		parsedSize, err := bytesize.Parse(cfg.Server.MaxProxyResponseSize)
615		if err != nil {
616			return nil, log.WrapErrWithFields(err, "invalid server.max_proxy_response_size configuration", map[string]any{"value": cfg.Server.MaxProxyResponseSize})
617		}
618		maxProxyResponseSize = parsedSize
619	}
620
621	maxConcurrentConns := cfg.Server.MaxConcurrentConns
622	if maxConcurrentConns < 0 {
623		maxConcurrentConns = 10000 // default when explicitly set to -1
624	}
625	// 0 means no limit (as documented in proxy.Config)
626
627	registryDomain, _ := resolveRegistryDomains(cfg)
628
629	return &proxyConfigResult{
630		proxyConfig: proxy.Config{
631			RegistryDomain:     registryDomain,
632			RegistryPort:       cfg.Server.RegistryPort,
633			MaxBodySize:        maxProxyBodySize,
634			MaxResponseSize:    maxProxyResponseSize,
635			MaxConcurrentConns: maxConcurrentConns,
636		},
637		maxBlobChunkSize: maxBlobChunkSize,
638		maxBlobSize:      maxBlobSize,
639	}, nil
640}
641
642// buildDNSConfig parses the raw dns config section into a publictls.DNSConfig.
643func buildDNSConfig(cfg Config) (publictls.DNSConfig, error) {
644	defaults := publictls.DefaultDNSConfig()
645
646	resolvers := cfg.DNS.Resolvers
647	if len(resolvers) == 0 {
648		resolvers = defaults.Resolvers
649	}
650
651	propagationTimeout := defaults.PropagationTimeout
652	if cfg.DNS.PropagationTimeout != "" {
653		parsed, err := time.ParseDuration(cfg.DNS.PropagationTimeout)
654		if err != nil {
655			return publictls.DNSConfig{}, fmt.Errorf("invalid dns.propagation_timeout: %w", err)
656		}
657		propagationTimeout = parsed
658	}
659
660	pollingInterval := defaults.PollingInterval
661	if cfg.DNS.PollingInterval != "" {
662		parsed, err := time.ParseDuration(cfg.DNS.PollingInterval)
663		if err != nil {
664			return publictls.DNSConfig{}, fmt.Errorf("invalid dns.polling_interval: %w", err)
665		}
666		pollingInterval = parsed
667	}
668
669	dnsCfg := publictls.DNSConfig{
670		Resolvers:          append([]string(nil), resolvers...),
671		PropagationTimeout: propagationTimeout,
672		PollingInterval:    pollingInterval,
673	}
674	if err := dnsCfg.Validate(); err != nil {
675		return publictls.DNSConfig{}, err
676	}
677	return dnsCfg, nil
678}
679
680func buildContainerServiceConfig(_ context.Context, v *viper.Viper, _ Config, _ *services, _ zerowrap.Logger) (container.Config, error) {
681	return container.Config{
682		NetworkPrefix: v.GetString("network_isolation.network_prefix"),
683	}, nil
684}
685
686// createContainerService creates the container service with configuration.
687func createContainerService(ctx context.Context, v *viper.Viper, cfg Config, svc *services, log zerowrap.Logger) (*container.Service, error) {
688	containerConfig, err := buildContainerServiceConfig(ctx, v, cfg, svc, log)
689	if err != nil {
690		return nil, err
691	}
692	return container.NewService(svc.runtime, svc.eventBus, svc.logWriter, containerConfig), nil
693}
694
695// registerEventHandlers registers event handlers. Push-triggered
696// route creation (auto-route, previews, image-pushed deploy) is
697// retired: pushes transfer OCI content only and apps deploy
698// explicitly via `gordon apps deploy`. The pre-v3 route-engine
699// deploy arms (config-reload redeploy, manual SIGUSR2 deploy,
700// secrets-changed redeploy) are removed with the declarative-apps
701// cutover: reload never activates app state, and app secret changes
702// take effect on explicit deploy.
703func registerEventHandlers(ctx context.Context, svc *services) (func(), error) {
704	// Proxy cache invalidation on config reload (clears stale targets for removed routes)
705	configReloadProxyHandler := proxy.NewConfigReloadProxyHandler(ctx, svc.proxySvc)
706	if err := svc.eventBus.Subscribe(configReloadProxyHandler); err != nil {
707		return nil, fmt.Errorf("failed to subscribe config reload proxy handler: %w", err)
708	}
709
710	cleanup := func() {
711		if svc.reloadCoordinator != nil {
712			svc.reloadCoordinator.Stop()
713		}
714	}
715
716	return cleanup, nil
717}

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