1package app
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "time"
9
10 "github.com/bnema/zerowrap"
11 "github.com/spf13/viper"
12
13 "github.com/bnema/gordon/internal/adapters/out/telemetry"
14 "github.com/bnema/gordon/internal/domain"
15 "github.com/bnema/gordon/internal/usecase/publictls"
16 servicecfg "github.com/bnema/gordon/internal/usecase/services"
17 "github.com/bnema/gordon/internal/usecase/traffic"
18)
19
20// Config holds the application configuration.
21type Config struct {
22 Server struct {
23 Port int `mapstructure:"port"`
24 RegistryPort int `mapstructure:"registry_port"`
25 GordonDomain string `mapstructure:"gordon_domain"`
26 RegistryDomain string `mapstructure:"registry_domain"`
27 LegacyRegistryDomains []string `mapstructure:"legacy_registry_domains"`
28 TLSPort int `mapstructure:"tls_port"`
29 TLSCertFile string `mapstructure:"tls_cert_file"`
30 TLSKeyFile string `mapstructure:"tls_key_file"`
31 ForceHTTPSRedirect bool `mapstructure:"force_https_redirect"`
32 DataDir string `mapstructure:"data_dir"`
33 MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB"
34 MaxBlobChunkSize string `mapstructure:"max_blob_chunk_size"` // e.g., "512MB", "1GB"
35 MaxBlobSize string `mapstructure:"max_blob_size"` // e.g., "1GB", "2GB"
36 MaxProxyResponseSize string `mapstructure:"max_proxy_response_size"` // e.g., "1GB", "0" for no limit
37 MaxConcurrentConns int `mapstructure:"max_concurrent_connections"`
38 RegistryAllowedIPs []string `mapstructure:"registry_allowed_ips"`
39 ProxyAllowedIPs []string `mapstructure:"proxy_allowed_ips"`
40 RegistryListenAddr string `mapstructure:"registry_listen_address"`
41
42 // HTTP holds the registry HTTP server timeouts (Go duration strings).
43 HTTP struct {
44 ReadHeaderTimeout string `mapstructure:"read_header_timeout"`
45 ReadTimeout string `mapstructure:"read_timeout"`
46 WriteTimeout string `mapstructure:"write_timeout"`
47 IdleTimeout string `mapstructure:"idle_timeout"`
48 } `mapstructure:"http"`
49 } `mapstructure:"server"`
50
51 Apps struct {
52 // RevisionRetention bounds unreferenced-revision GC per app.
53 // Zero or negative restores the compiled default (8).
54 RevisionRetention int `mapstructure:"revision_retention"`
55 } `mapstructure:"apps"`
56
57 // AppMounts declares administrative bind policies keyed by stable mount
58 // name, referenced by name from an app manifest's [[services.<name>.bind]].
59 AppMounts map[string]AppMountPolicy `mapstructure:"app_mounts"`
60
61 // AppDevices declares administrative device grants keyed by stable
62 // logical device name, referenced by name from an app manifest's
63 // `devices` list.
64 AppDevices map[string]AppDevicePolicy `mapstructure:"app_devices"`
65
66 // AppCapabilities declares administrative Linux capability grants keyed
67 // by stable logical name, referenced by name from an app manifest's
68 // `capabilities` list.
69 AppCapabilities map[string]AppCapabilityPolicy `mapstructure:"app_capabilities"`
70
71 Logging struct {
72 Level string `mapstructure:"level"`
73 Format string `mapstructure:"format"`
74 File struct {
75 Enabled bool `mapstructure:"enabled"`
76 Path string `mapstructure:"path"`
77 MaxSize int `mapstructure:"max_size"`
78 MaxBackups int `mapstructure:"max_backups"`
79 MaxAge int `mapstructure:"max_age"`
80 } `mapstructure:"file"`
81 ContainerLogs struct {
82 Enabled bool `mapstructure:"enabled"`
83 Dir string `mapstructure:"dir"`
84 MaxSize int `mapstructure:"max_size"`
85 MaxBackups int `mapstructure:"max_backups"`
86 MaxAge int `mapstructure:"max_age"`
87 } `mapstructure:"container_logs"`
88 AccessLog struct {
89 Enabled bool `mapstructure:"enabled"`
90 Format string `mapstructure:"format"`
91 Output string `mapstructure:"output"`
92 FilePath string `mapstructure:"file_path"`
93 MaxSize int `mapstructure:"max_size"`
94 MaxBackups int `mapstructure:"max_backups"`
95 MaxAge int `mapstructure:"max_age"`
96 ExcludeHealthChecks bool `mapstructure:"exclude_health_checks"`
97 SyslogIdentifier string `mapstructure:"syslog_identifier"`
98 } `mapstructure:"access_log"`
99 } `mapstructure:"logging"`
100
101 Volumes struct {
102 AutoCreate bool `mapstructure:"auto_create"`
103 Prefix string `mapstructure:"prefix"`
104 Preserve bool `mapstructure:"preserve"`
105 } `mapstructure:"volumes"`
106
107 Auth struct {
108 Enabled bool `mapstructure:"enabled"`
109 Type string `mapstructure:"type"` // only "token" is supported
110 SecretsBackend string `mapstructure:"secrets_backend"` // "pass", "sops", or "unsafe"
111 Username string `mapstructure:"username"`
112 TokenSecret string `mapstructure:"token_secret"` // path in secrets backend
113 TokenExpiry string `mapstructure:"token_expiry"` // e.g., "720h", "30d"
114 AccessTokenTTL string `mapstructure:"access_token_ttl"` // e.g., "15m", "30m" (default: 15m)
115 } `mapstructure:"auth"`
116
117 API struct {
118 RateLimit struct {
119 Enabled bool `mapstructure:"enabled"`
120 GlobalRPS float64 `mapstructure:"global_rps"`
121 PerIPRPS float64 `mapstructure:"per_ip_rps"`
122 Burst int `mapstructure:"burst"`
123 TrustedProxies []string `mapstructure:"trusted_proxies"`
124 } `mapstructure:"rate_limit"`
125 } `mapstructure:"api"`
126
127 EntryPoints map[string]traffic.EntryPointConfig `mapstructure:"entrypoints"`
128 Traffic traffic.Config `mapstructure:"traffic"`
129 NetworkServices []traffic.NetworkServiceConfig `mapstructure:"network_services"`
130 Services []servicecfg.Config `mapstructure:"services"`
131
132 Backups struct {
133 // Legacy database backup keys. Prefer backups.databases.* for new configs.
134 Enabled bool `mapstructure:"enabled"`
135 Schedule string `mapstructure:"schedule"`
136 StorageDir string `mapstructure:"storage_dir"`
137 Retention struct {
138 Hourly int `mapstructure:"hourly"`
139 Daily int `mapstructure:"daily"`
140 Weekly int `mapstructure:"weekly"`
141 Monthly int `mapstructure:"monthly"`
142 } `mapstructure:"retention"`
143 Databases struct {
144 Enabled bool `mapstructure:"enabled"`
145 Schedule string `mapstructure:"schedule"`
146 Time string `mapstructure:"time"`
147 StorageDir string `mapstructure:"storage_dir"`
148 Retention struct {
149 Hourly int `mapstructure:"hourly"`
150 Daily int `mapstructure:"daily"`
151 Weekly int `mapstructure:"weekly"`
152 Monthly int `mapstructure:"monthly"`
153 } `mapstructure:"retention"`
154 } `mapstructure:"databases"`
155 Volumes struct {
156 Enabled bool `mapstructure:"enabled"`
157 Schedule string `mapstructure:"schedule"`
158 Time string `mapstructure:"time"`
159 Compression string `mapstructure:"compression"`
160 Timeout string `mapstructure:"timeout"`
161 MaxConcurrency int `mapstructure:"max_concurrency"`
162 HelperImage string `mapstructure:"helper_image"`
163 S3 struct {
164 Bucket string `mapstructure:"bucket"`
165 Region string `mapstructure:"region"`
166 Prefix string `mapstructure:"prefix"`
167 Endpoint string `mapstructure:"endpoint"`
168 PathStyle bool `mapstructure:"path_style"`
169 SSEAlgorithm string `mapstructure:"sse_algorithm"`
170 SSEKMSKeyID string `mapstructure:"sse_kms_key_id"`
171 StorageClass string `mapstructure:"storage_class"`
172 } `mapstructure:"s3"`
173 Retention struct {
174 Keep int `mapstructure:"keep"`
175 } `mapstructure:"retention"`
176 } `mapstructure:"volumes"`
177 } `mapstructure:"backups"`
178
179 Images struct {
180 AllowedRegistries []string `mapstructure:"allowed_registries"`
181 RequireDigest bool `mapstructure:"require_digest"`
182 Prune struct {
183 Enabled bool `mapstructure:"enabled"`
184 Schedule string `mapstructure:"schedule"`
185 KeepLast int `mapstructure:"keep_last"`
186 } `mapstructure:"prune"`
187 } `mapstructure:"images"`
188
189 Containers struct {
190 MemoryLimit string `mapstructure:"memory_limit"` // e.g., "512MB", "1GB"
191 CPULimit float64 `mapstructure:"cpu_limit"` // CPU cores, e.g., 1.0 = 1 core
192 PidsLimit int64 `mapstructure:"pids_limit"` // e.g., 512
193 SecurityProfile string `mapstructure:"security_profile"` // compat or strict
194 } `mapstructure:"containers"`
195
196 NetworkIsolation struct {
197 Enabled bool `mapstructure:"enabled"`
198 Prefix string `mapstructure:"network_prefix"`
199 Internal bool `mapstructure:"internal"`
200 } `mapstructure:"network_isolation"`
201
202 Telemetry telemetry.Config `mapstructure:"telemetry"`
203
204 TLS struct {
205 ACME struct {
206 Enabled bool `mapstructure:"enabled"`
207 Email string `mapstructure:"email"`
208 Challenge string `mapstructure:"challenge"`
209 ObtainBatchSize int `mapstructure:"obtain_batch_size"`
210 } `mapstructure:"acme"`
211 } `mapstructure:"tls"`
212
213 DNS struct {
214 Resolvers []string `mapstructure:"resolvers"`
215 PropagationTimeout string `mapstructure:"propagation_timeout"`
216 PollingInterval string `mapstructure:"polling_interval"`
217 } `mapstructure:"dns"`
218}
219
220func warnDeprecatedConfigKeys(v *viper.Viper, log zerowrap.Logger) {
221 for _, key := range []string{"server.tls_enabled", "server.force_hsts"} {
222 if v.IsSet(key) {
223 log.Warn().Str("key", key).Msg("deprecated config key — Gordon now uses an internal CA with automatic TLS; remove this from your config")
224 }
225 }
226}
227
228// initConfig loads configuration from file.
229func initConfig(configPath string) (*viper.Viper, Config, error) {
230 v := viper.New()
231 if err := loadConfig(v, configPath); err != nil {
232 return nil, Config{}, fmt.Errorf("failed to load config: %w", err)
233 }
234
235 var cfg Config
236 if err := v.Unmarshal(&cfg); err != nil {
237 return nil, Config{}, fmt.Errorf("failed to unmarshal config: %w", err)
238 }
239 if err := validateEntrypointMigration(v, cfg); err != nil {
240 return nil, Config{}, err
241 }
242 if err := validateRetiredAppConfig(v); err != nil {
243 return nil, Config{}, err
244 }
245 if err := validateHTTPTimeouts(cfg); err != nil {
246 return nil, Config{}, err
247 }
248 if _, err := buildAppMountPolicies(cfg); err != nil {
249 return nil, Config{}, err
250 }
251 if err := validateDeviceConfig(v, cfg); err != nil {
252 return nil, Config{}, err
253 }
254 if err := validateCapabilityConfig(v, cfg); err != nil {
255 return nil, Config{}, err
256 }
257
258 return v, cfg, nil
259}
260
261// retiredAppConfigKeys are pre-v3 application keys removed with the
262// declarative-apps cutover. Presence fails boot/reload closed with a
263// config-retired diagnostic naming the fix — never a silent migration.
264// Keys that remain live installation-level settings are deliberately
265// absent: top-level "services" still declares standalone L4 workloads
266// (see config.services), so it must never be rejected here.
267var retiredAppConfigKeys = []struct {
268 key string
269 hint string
270}{
271 {"routes", "declare [[services.<name>.http]] in an app file, then 'gordon apps apply'"},
272 {"attachments", "declare [services.<name>] + volumes; attachments are removed"},
273 {"network_groups", "declare [[network.shared]] with ownership verification"},
274 {"service_routes", "declare [[services.<name>.http]] instead"},
275 {"auto", "feature removed; declare explicit interfaces"},
276 {"auto_route", "feature removed; declare explicit interfaces"},
277 {"auto_route_allowed_domains", "feature removed; declare explicit interfaces"},
278 {"previews", "staging is an ordinary app file"},
279 {"env", "remove the [env] section; declare [env] and [services.<name>.secrets] in app files"},
280 {"backups.volumes.interval", "use backups.volumes.schedule and backups.volumes.time (server local time)"},
281}
282
283// validateRetiredAppConfig rejects obsolete application configuration
284// BEFORE any app/runtime mutation, at startup and reload. InConfig
285// (not IsSet) targets explicit file keys only — installation defaults
286// registered via SetDefault must not trip the rejection.
287func validateRetiredAppConfig(v *viper.Viper) error {
288 diagnostics := make([]string, 0, len(retiredAppConfigKeys))
289 for _, retired := range retiredAppConfigKeys {
290 if v.InConfig(retired.key) {
291 diagnostics = append(diagnostics, fmt.Sprintf("key %q was removed in v3; %s", retired.key, retired.hint))
292 }
293 }
294 if len(diagnostics) > 0 {
295 return fmt.Errorf("config-retired: %s", strings.Join(diagnostics, "; "))
296 }
297 return nil
298}
299
300func validateEntrypointMigration(v *viper.Viper, cfg Config) error {
301 if len(cfg.EntryPoints) > 0 {
302 return nil
303 }
304
305 legacyKeys := make([]string, 0, 2)
306 for _, key := range []string{"server.port", "server.tls_port"} {
307 if v.IsSet(key) {
308 legacyKeys = append(legacyKeys, key)
309 }
310 }
311 if len(legacyKeys) == 0 {
312 return nil
313 }
314
315 return fmt.Errorf("legacy %s configuration requires at least one [entrypoints] entry; see docs/upgrading.md", strings.Join(legacyKeys, " or "))
316}
317
318// resolveLogFilePath returns the configured log file path or a default.
319func resolveLogFilePath(cfg Config) string {
320 if cfg.Logging.File.Path != "" {
321 return cfg.Logging.File.Path
322 }
323 dataDir := cfg.Server.DataDir
324 if dataDir == "" {
325 dataDir = DefaultDataDir()
326 }
327 return filepath.Join(dataDir, "logs", "gordon.log")
328}
329
330// resolveRuntimeConfig converts a server.runtime config value to a socket path.
331// "auto" or "" means auto-detect.
332// Named runtimes ("podman", "docker") are resolved to well-known socket paths.
333// URI schemes (unix://) are stripped so callers receive a bare path.
334func resolveRuntimeConfig(value string) string {
335 if value == "" || value == "auto" {
336 return ""
337 }
338 // Named runtimes: resolve to well-known socket paths.
339 switch value {
340 case "podman":
341 // Check XDG_RUNTIME_DIR first (rootless Podman).
342 if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" {
343 candidate := filepath.Join(xdg, "podman", "podman.sock")
344 if _, err := os.Stat(candidate); err == nil {
345 return candidate
346 }
347 }
348 // Fallback to system-wide Podman socket.
349 return "/run/podman/podman.sock"
350 case "docker":
351 return "/var/run/docker.sock"
352 }
353 // Explicit socket path — strip URI scheme if present.
354 if socketPath, ok := strings.CutPrefix(value, "unix://"); ok {
355 return socketPath
356 }
357 return value
358}
359
360func resolveDataDir(dataDir string) string {
361 if dataDir == "" {
362 return DefaultDataDir()
363 }
364 return dataDir
365}
366
367func resolveRegistryDomains(cfg Config) (string, []string) {
368 registryDomain := cfg.Server.RegistryDomain
369 if registryDomain == "" {
370 registryDomain = cfg.Server.GordonDomain
371 }
372 return registryDomain, append([]string{}, cfg.Server.LegacyRegistryDomains...)
373}
374
375// loadConfig loads configuration from file and sets defaults.
376func loadConfig(v *viper.Viper, configPath string) error {
377 v.SetDefault("server.registry_port", 5000)
378 v.SetDefault("server.legacy_registry_domains", []string{})
379 v.SetDefault("server.tls_cert_file", "")
380 v.SetDefault("server.tls_key_file", "")
381 v.SetDefault("tls.acme.enabled", false)
382 v.SetDefault("tls.acme.email", "")
383 v.SetDefault("tls.acme.challenge", "auto")
384 v.SetDefault("tls.acme.obtain_batch_size", 1)
385 v.SetDefault("dns.resolvers", publictls.DefaultDNSResolvers)
386 v.SetDefault("dns.propagation_timeout", "5m")
387 v.SetDefault("dns.polling_interval", "5s")
388 v.SetDefault("server.force_https_redirect", false)
389 v.SetDefault("server.data_dir", DefaultDataDir())
390 v.SetDefault("server.runtime", "auto")
391 v.SetDefault("logging.level", "info")
392 v.SetDefault("logging.format", "console")
393 v.SetDefault("logging.file.enabled", false)
394 v.SetDefault("logging.file.max_size", 100)
395 v.SetDefault("logging.file.max_backups", 3)
396 v.SetDefault("logging.file.max_age", 28)
397 v.SetDefault("logging.container_logs.enabled", true)
398 v.SetDefault("logging.container_logs.dir", "")
399 v.SetDefault("logging.container_logs.max_size", 100)
400 v.SetDefault("logging.container_logs.max_backups", 3)
401 v.SetDefault("logging.container_logs.max_age", 28)
402 v.SetDefault("logging.access_log.enabled", false)
403 v.SetDefault("logging.access_log.format", "json")
404 v.SetDefault("logging.access_log.output", "stdout")
405 v.SetDefault("logging.access_log.file_path", "")
406 v.SetDefault("logging.access_log.max_size", 100)
407 v.SetDefault("logging.access_log.max_backups", 3)
408 v.SetDefault("logging.access_log.max_age", 28)
409 v.SetDefault("logging.access_log.exclude_health_checks", true)
410 v.SetDefault("logging.access_log.syslog_identifier", "gordon-access")
411 v.SetDefault("auth.enabled", true)
412 // Note: auth.type defaults to "token" (the only supported mode)
413 v.SetDefault("auth.secrets_backend", "")
414 v.SetDefault("auth.token_expiry", "720h")
415 v.SetDefault("api.rate_limit.enabled", true)
416 v.SetDefault("api.rate_limit.global_rps", 500)
417 v.SetDefault("api.rate_limit.per_ip_rps", 50)
418 v.SetDefault("api.rate_limit.burst", 100)
419 v.SetDefault("auto_route.enabled", false)
420 v.SetDefault("network_isolation.enabled", true)
421 v.SetDefault("network_isolation.network_prefix", "gordon")
422 v.SetDefault("network_isolation.internal", false)
423 v.SetDefault("volumes.auto_create", true)
424 v.SetDefault("volumes.prefix", "gordon")
425 v.SetDefault("volumes.preserve", true)
426 v.SetDefault("backups.databases.enabled", false)
427 v.SetDefault("backups.databases.schedule", string(domain.ScheduleDaily))
428 v.SetDefault("backups.databases.time", "")
429 v.SetDefault("backups.databases.storage_dir", "")
430 v.SetDefault("backups.databases.retention.hourly", 0)
431 v.SetDefault("backups.databases.retention.daily", 0)
432 v.SetDefault("backups.databases.retention.weekly", 0)
433 v.SetDefault("backups.databases.retention.monthly", 0)
434 v.SetDefault("backups.volumes.enabled", false)
435 v.SetDefault("backups.volumes.schedule", string(domain.ScheduleDaily))
436 v.SetDefault("backups.volumes.time", "")
437 v.SetDefault("backups.volumes.compression", string(domain.VolumeBackupCompressionGzip))
438 v.SetDefault("backups.volumes.timeout", "2h")
439 v.SetDefault("backups.volumes.max_concurrency", 2)
440 v.SetDefault("backups.volumes.helper_image", "alpine:3.20")
441 v.SetDefault("backups.volumes.s3.bucket", "")
442 v.SetDefault("backups.volumes.s3.region", "")
443 v.SetDefault("backups.volumes.s3.prefix", "")
444 v.SetDefault("backups.volumes.s3.endpoint", "")
445 v.SetDefault("backups.volumes.s3.path_style", false)
446 v.SetDefault("backups.volumes.s3.sse_algorithm", "")
447 v.SetDefault("backups.volumes.s3.sse_kms_key_id", "")
448 v.SetDefault("backups.volumes.s3.storage_class", "")
449 v.SetDefault("backups.volumes.retention.keep", 14)
450 v.SetDefault("images.allowed_registries", []string{})
451 v.SetDefault("images.require_digest", false)
452 v.SetDefault("images.prune.enabled", false)
453 v.SetDefault("images.prune.schedule", string(domain.ScheduleDaily))
454 v.SetDefault("images.prune.keep_last", domain.DefaultImagePruneKeepLast)
455 v.SetDefault("containers.security_profile", "compat")
456 v.SetDefault("telemetry.enabled", false)
457 v.SetDefault("telemetry.endpoint", "")
458 v.SetDefault("telemetry.auth_token", "")
459 v.SetDefault("telemetry.traces", true)
460 v.SetDefault("telemetry.metrics", true)
461 v.SetDefault("telemetry.logs", true)
462 v.SetDefault("telemetry.trace_sample_rate", 1.0)
463
464 v.SetDefault("server.max_concurrent_connections", -1) // -1 = use default (10000), 0 = no limit
465 v.SetDefault("server.registry_allowed_ips", []string{})
466 v.SetDefault("server.proxy_allowed_ips", []string{})
467 v.SetDefault("server.registry_listen_address", "")
468
469 // HTTP timeouts. Every key needs a default so GORDON_* env overrides apply.
470 v.SetDefault("server.http.read_header_timeout", domain.DefaultRegistryReadHeaderTimeout.String())
471 v.SetDefault("server.http.read_timeout", domain.DefaultRegistryReadTimeout.String())
472 v.SetDefault("server.http.write_timeout", domain.DefaultRegistryWriteTimeout.String())
473 v.SetDefault("server.http.idle_timeout", domain.DefaultRegistryIdleTimeout.String())
474 v.SetDefault("traffic.http.read_header_timeout", domain.DefaultEntryPointReadHeaderTimeout.String())
475 v.SetDefault("traffic.http.idle_timeout", domain.DefaultEntryPointIdleTimeout.String())
476 v.SetDefault("traffic.http.transfer_idle_timeout", domain.DefaultEntryPointTransferIdleTimeout.String())
477 v.SetDefault("traffic.tcp.dial_timeout", "10s")
478 v.SetDefault("traffic.tcp.idle_timeout", "5m")
479 v.SetDefault("traffic.tcp.drain_timeout", "30s")
480 v.SetDefault("traffic.udp.idle_timeout", "30s")
481 v.SetDefault("traffic.udp.drain_timeout", "30s")
482 v.SetDefault("traffic.tcp.max_connections", 0) // 0 = runtime default
483 v.SetDefault("traffic.udp.max_sessions", 0) // 0 = runtime default
484
485 ConfigureViper(v, configPath)
486
487 if err := v.ReadInConfig(); err != nil {
488 if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
489 return fmt.Errorf("failed to read config file: %w", err)
490 }
491 }
492
493 v.SetEnvPrefix("GORDON")
494 v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
495 v.AutomaticEnv()
496
497 return nil
498}
499
500// validateHTTPTimeouts rejects malformed, zero, or negative HTTP timeouts at
501// config load, before any listener is started.
502func validateHTTPTimeouts(cfg Config) error {
503 if _, err := buildRegistryHTTPTimeouts(cfg); err != nil {
504 return err
505 }
506 if _, err := traffic.ParseHTTPOptions(cfg.Traffic.HTTP); err != nil {
507 return err
508 }
509 return nil
510}
511
512// buildRegistryHTTPTimeouts parses server.http into validated registry server
513// timeouts. Empty values select the defaults; malformed, zero, or negative
514// values are rejected.
515func buildRegistryHTTPTimeouts(cfg Config) (domain.RegistryHTTPTimeouts, error) {
516 defaults := domain.DefaultRegistryHTTPTimeouts()
517 var (
518 t domain.RegistryHTTPTimeouts
519 err error
520 )
521 if t.ReadHeaderTimeout, err = parsePositiveDuration(cfg.Server.HTTP.ReadHeaderTimeout, defaults.ReadHeaderTimeout, "server.http.read_header_timeout"); err != nil {
522 return t, err
523 }
524 if t.ReadTimeout, err = parsePositiveDuration(cfg.Server.HTTP.ReadTimeout, defaults.ReadTimeout, "server.http.read_timeout"); err != nil {
525 return t, err
526 }
527 if t.WriteTimeout, err = parsePositiveDuration(cfg.Server.HTTP.WriteTimeout, defaults.WriteTimeout, "server.http.write_timeout"); err != nil {
528 return t, err
529 }
530 if t.IdleTimeout, err = parsePositiveDuration(cfg.Server.HTTP.IdleTimeout, defaults.IdleTimeout, "server.http.idle_timeout"); err != nil {
531 return t, err
532 }
533 return t, t.Validate()
534}
535
536func parsePositiveDuration(value string, fallback time.Duration, key string) (time.Duration, error) {
537 value = strings.TrimSpace(value)
538 if value == "" {
539 return fallback, nil
540 }
541 d, err := time.ParseDuration(value)
542 if err != nil {
543 return 0, fmt.Errorf("invalid %s: %w", key, err)
544 }
545 if d <= 0 {
546 return 0, fmt.Errorf("invalid %s: must be positive", key)
547 }
548 return d, nil
549}