v3.4.0 593c1c3

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:       backupServicePort(si.svc.backupSvc),
435		VolumeBackupSvc: volumeBackupServicePort(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// backupServicePort returns the database backup service as a port, or an
446// untyped nil when backups are disabled. Storing a nil *backup.Service in
447// the interface would make it non-nil and handlers would call into it.
448func backupServicePort(svc *backup.Service) in.BackupService {
449	if svc == nil {
450		return nil
451	}
452	return svc
453}
454
455// volumeBackupServicePort is backupServicePort for volume backups.
456func volumeBackupServicePort(svc *backup.VolumeService) in.VolumeBackupService {
457	if svc == nil {
458		return nil
459	}
460	return svc
461}
462
463// caInfoProvider returns the internal CA as a CAInfoService, or an untyped nil
464// when internal TLS is disabled so the admin handler sees a nil interface.
465func (si *serviceInit) caInfoProvider() in.CAInfoService {
466	if si.svc.caAdapter == nil {
467		return nil
468	}
469	return si.svc.caAdapter
470}
471
472// injectTelemetryMetrics creates and injects OTel metrics into services when
473// telemetry is enabled. Skipped otherwise to avoid unnecessary allocations.
474func injectTelemetryMetrics(cfg Config, svc *services, log zerowrap.Logger) {
475	if !cfg.Telemetry.Enabled || !cfg.Telemetry.Metrics {
476		return
477	}
478	gordonMetrics, err := telemetry.NewMetrics()
479	if err != nil {
480		log.Warn().Err(err).Msg("failed to create telemetry metrics, continuing without metrics")
481		return
482	}
483	svc.registrySvc.SetMetrics(gordonMetrics)
484	svc.eventBus.SetMetrics(gordonMetrics)
485	svc.metrics = gordonMetrics
486}
487
488// observeManagedContainers exports the managed-container gauge from app
489// state. No-op when metrics are disabled.
490func observeManagedContainers(metrics *telemetry.Metrics, state out.AppStateReader, log zerowrap.Logger) {
491	if metrics == nil {
492		return
493	}
494	if err := metrics.ObserveManagedContainers(func(ctx context.Context) (int64, error) {
495		return apps.CountManagedContainers(ctx, state)
496	}); err != nil {
497		log.Warn().Err(err).Msg("failed to register managed container gauge")
498	}
499}
500
501// createOutputAdapters creates the container runtime and event bus.
502func createOutputAdapters(ctx context.Context, log zerowrap.Logger, runtimeSocket string) (*docker.Runtime, *eventbus.InMemory, error) {
503	detection := docker.DetectRuntimeSocket(runtimeSocket)
504
505	var runtime *docker.Runtime
506	var err error
507
508	switch detection.Source {
509	case "none":
510		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")
511	case "DOCKER_HOST_passthrough":
512		detection.RuntimeName = "docker"
513		runtime, err = docker.NewRuntime()
514	default:
515		if detection.SocketPath != "" {
516			runtime, err = docker.NewRuntimeWithSocket(detection.SocketPath)
517		} else {
518			runtime, err = docker.NewRuntime()
519		}
520	}
521	if err != nil {
522		return nil, nil, log.WrapErr(err, "failed to create container runtime")
523	}
524
525	if err := runtime.Ping(ctx); err != nil {
526		return nil, nil, log.WrapErr(err, fmt.Sprintf("container runtime not available (detected: %s via %s)", detection.RuntimeName, detection.Source))
527	}
528
529	runtimeVersion, _ := runtime.Version(ctx)
530	log.Info().
531		Str("runtime", detection.RuntimeName).
532		Str("version", runtimeVersion).
533		Str("source", detection.Source).
534		Msg("container runtime initialized")
535
536	eventBus := eventbus.NewInMemory(100, log)
537
538	return runtime, eventBus, nil
539}
540
541// createStorage creates blob and manifest storage.
542func createStorage(cfg Config, log zerowrap.Logger) (*filesystem.BlobStorage, *filesystem.ManifestStorage, error) {
543	dataDir := cfg.Server.DataDir
544	if dataDir == "" {
545		dataDir = DefaultDataDir()
546	}
547
548	registryDir := filepath.Join(dataDir, "registry")
549
550	blobStorage, err := filesystem.NewBlobStorage(registryDir, log)
551	if err != nil {
552		return nil, nil, log.WrapErr(err, "failed to create blob storage")
553	}
554
555	manifestStorage, err := filesystem.NewManifestStorage(registryDir, log)
556	if err != nil {
557		return nil, nil, log.WrapErr(err, "failed to create manifest storage")
558	}
559
560	return blobStorage, manifestStorage, nil
561}
562
563// createLogWriter creates the container log writer.
564func createLogWriter(cfg Config, log zerowrap.Logger) (*logwriter.LogWriter, error) {
565	if !cfg.Logging.ContainerLogs.Enabled {
566		log.Debug().Msg("container log collection disabled")
567		return nil, nil
568	}
569
570	// Determine log directory
571	logDir := cfg.Logging.ContainerLogs.Dir
572	if logDir == "" {
573		dataDir := cfg.Server.DataDir
574		if dataDir == "" {
575			dataDir = DefaultDataDir()
576		}
577		logDir = filepath.Join(dataDir, "logs", "containers")
578	}
579
580	writer, err := logwriter.New(logwriter.Config{
581		Dir:        logDir,
582		MaxSize:    cfg.Logging.ContainerLogs.MaxSize,
583		MaxBackups: cfg.Logging.ContainerLogs.MaxBackups,
584		MaxAge:     cfg.Logging.ContainerLogs.MaxAge,
585	})
586	if err != nil {
587		return nil, log.WrapErr(err, "failed to create container log writer")
588	}
589
590	log.Info().Str("dir", logDir).Msg("container log collection enabled")
591	return writer, nil
592}
593
594// proxyConfigResult holds parsed proxy and blob chunk size config.
595type proxyConfigResult struct {
596	proxyConfig      proxy.Config
597	maxBlobChunkSize int64
598	maxBlobSize      int64
599}
600
601// buildProxyConfig parses size-related config fields and builds the proxy config.
602func buildProxyConfig(cfg Config, log zerowrap.Logger) (*proxyConfigResult, error) {
603	maxProxyBodySize := int64(512 << 20) // 512MB default
604	if cfg.Server.MaxProxyBodySize != "" {
605		parsedSize, err := bytesize.Parse(cfg.Server.MaxProxyBodySize)
606		if err != nil {
607			return nil, log.WrapErrWithFields(err, "invalid server.max_proxy_body_size configuration", map[string]any{"value": cfg.Server.MaxProxyBodySize})
608		}
609		maxProxyBodySize = parsedSize
610	}
611
612	maxBlobChunkSize := int64(registry.DefaultMaxBlobChunkSize)
613	if cfg.Server.MaxBlobChunkSize != "" {
614		parsedSize, err := bytesize.Parse(cfg.Server.MaxBlobChunkSize)
615		if err != nil {
616			return nil, log.WrapErrWithFields(err, "invalid server.max_blob_chunk_size configuration", map[string]any{"value": cfg.Server.MaxBlobChunkSize})
617		}
618		maxBlobChunkSize = parsedSize
619	}
620
621	maxBlobSize := int64(registry.DefaultMaxBlobSize)
622	if cfg.Server.MaxBlobSize != "" {
623		parsedSize, err := bytesize.Parse(cfg.Server.MaxBlobSize)
624		if err != nil {
625			return nil, log.WrapErrWithFields(err, "invalid server.max_blob_size configuration", map[string]any{"value": cfg.Server.MaxBlobSize})
626		}
627		maxBlobSize = parsedSize
628	}
629
630	maxProxyResponseSize := int64(1 << 30) // 1GB default
631	if cfg.Server.MaxProxyResponseSize != "" {
632		parsedSize, err := bytesize.Parse(cfg.Server.MaxProxyResponseSize)
633		if err != nil {
634			return nil, log.WrapErrWithFields(err, "invalid server.max_proxy_response_size configuration", map[string]any{"value": cfg.Server.MaxProxyResponseSize})
635		}
636		maxProxyResponseSize = parsedSize
637	}
638
639	maxConcurrentConns := cfg.Server.MaxConcurrentConns
640	if maxConcurrentConns < 0 {
641		maxConcurrentConns = 10000 // default when explicitly set to -1
642	}
643	// 0 means no limit (as documented in proxy.Config)
644
645	registryDomain, _ := resolveRegistryDomains(cfg)
646
647	return &proxyConfigResult{
648		proxyConfig: proxy.Config{
649			RegistryDomain:     registryDomain,
650			RegistryPort:       cfg.Server.RegistryPort,
651			MaxBodySize:        maxProxyBodySize,
652			MaxResponseSize:    maxProxyResponseSize,
653			MaxConcurrentConns: maxConcurrentConns,
654		},
655		maxBlobChunkSize: maxBlobChunkSize,
656		maxBlobSize:      maxBlobSize,
657	}, nil
658}
659
660// buildDNSConfig parses the raw dns config section into a publictls.DNSConfig.
661func buildDNSConfig(cfg Config) (publictls.DNSConfig, error) {
662	defaults := publictls.DefaultDNSConfig()
663
664	resolvers := cfg.DNS.Resolvers
665	if len(resolvers) == 0 {
666		resolvers = defaults.Resolvers
667	}
668
669	propagationTimeout := defaults.PropagationTimeout
670	if cfg.DNS.PropagationTimeout != "" {
671		parsed, err := time.ParseDuration(cfg.DNS.PropagationTimeout)
672		if err != nil {
673			return publictls.DNSConfig{}, fmt.Errorf("invalid dns.propagation_timeout: %w", err)
674		}
675		propagationTimeout = parsed
676	}
677
678	pollingInterval := defaults.PollingInterval
679	if cfg.DNS.PollingInterval != "" {
680		parsed, err := time.ParseDuration(cfg.DNS.PollingInterval)
681		if err != nil {
682			return publictls.DNSConfig{}, fmt.Errorf("invalid dns.polling_interval: %w", err)
683		}
684		pollingInterval = parsed
685	}
686
687	dnsCfg := publictls.DNSConfig{
688		Resolvers:          append([]string(nil), resolvers...),
689		PropagationTimeout: propagationTimeout,
690		PollingInterval:    pollingInterval,
691	}
692	if err := dnsCfg.Validate(); err != nil {
693		return publictls.DNSConfig{}, err
694	}
695	return dnsCfg, nil
696}
697
698func buildContainerServiceConfig(_ context.Context, v *viper.Viper, _ Config, _ *services, _ zerowrap.Logger) (container.Config, error) {
699	return container.Config{
700		NetworkPrefix: v.GetString("network_isolation.network_prefix"),
701	}, nil
702}
703
704// createContainerService creates the container service with configuration.
705func createContainerService(ctx context.Context, v *viper.Viper, cfg Config, svc *services, log zerowrap.Logger) (*container.Service, error) {
706	containerConfig, err := buildContainerServiceConfig(ctx, v, cfg, svc, log)
707	if err != nil {
708		return nil, err
709	}
710	return container.NewService(svc.runtime, svc.eventBus, svc.logWriter, containerConfig), nil
711}
712
713// registerEventHandlers registers event handlers. Push-triggered
714// route creation (auto-route, previews, image-pushed deploy) is
715// retired: pushes transfer OCI content only and apps deploy
716// explicitly via `gordon apps deploy`. The pre-v3 route-engine
717// deploy arms (config-reload redeploy, manual SIGUSR2 deploy,
718// secrets-changed redeploy) are removed with the declarative-apps
719// cutover: reload never activates app state, and app secret changes
720// take effect on explicit deploy.
721func registerEventHandlers(ctx context.Context, svc *services) (func(), error) {
722	// Proxy cache invalidation on config reload (clears stale targets for removed routes)
723	configReloadProxyHandler := proxy.NewConfigReloadProxyHandler(ctx, svc.proxySvc)
724	if err := svc.eventBus.Subscribe(configReloadProxyHandler); err != nil {
725		return nil, fmt.Errorf("failed to subscribe config reload proxy handler: %w", err)
726	}
727
728	cleanup := func() {
729		if svc.reloadCoordinator != nil {
730			svc.reloadCoordinator.Stop()
731		}
732	}
733
734	return cleanup, nil
735}

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