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}