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}