package main import ( "fmt" "net/url" "os" "strconv" "time" "github.com/distribution/distribution/v3/configuration" ) // loadConfigFromEnv builds a complete configuration from environment variables // This follows the same pattern as the hold service (no config files, only env vars) func loadConfigFromEnv() (*configuration.Configuration, error) { config := &configuration.Configuration{} // Version config.Version = configuration.MajorMinorVersion(0, 1) // Logging config.Log = buildLogConfig() // HTTP server httpConfig, err := buildHTTPConfig() if err != nil { return nil, fmt.Errorf("failed to build HTTP config: %w", err) } config.HTTP = httpConfig // Storage (fake in-memory placeholder - all real storage is proxied) config.Storage = buildStorageConfig() // Middleware (ATProto resolver) defaultHold := os.Getenv("ATCR_DEFAULT_HOLD") if defaultHold == "" { return nil, fmt.Errorf("ATCR_DEFAULT_HOLD is required") } config.Middleware = buildMiddlewareConfig(defaultHold) // Auth baseURL := getBaseURL(httpConfig.Addr) authConfig, err := buildAuthConfig(baseURL) if err != nil { return nil, fmt.Errorf("failed to build auth config: %w", err) } config.Auth = authConfig // Health checks config.Health = buildHealthConfig() return config, nil } // buildLogConfig creates logging configuration from environment variables func buildLogConfig() configuration.Log { level := getEnvOrDefault("ATCR_LOG_LEVEL", "info") formatter := getEnvOrDefault("ATCR_LOG_FORMATTER", "text") return configuration.Log{ Level: configuration.Loglevel(level), Formatter: formatter, Fields: map[string]interface{}{ "service": "atcr-appview", }, } } // buildHTTPConfig creates HTTP server configuration from environment variables func buildHTTPConfig() (configuration.HTTP, error) { addr := getEnvOrDefault("ATCR_HTTP_ADDR", ":5000") debugAddr := getEnvOrDefault("ATCR_DEBUG_ADDR", ":5001") return configuration.HTTP{ Addr: addr, Headers: map[string][]string{ "X-Content-Type-Options": {"nosniff"}, }, Debug: configuration.Debug{ Addr: debugAddr, }, }, nil } // buildStorageConfig creates a fake in-memory storage config // This is required for distribution validation but is never actually used // All storage is routed through middleware to ATProto (manifests) and hold services (blobs) func buildStorageConfig() configuration.Storage { storage := configuration.Storage{} // Use in-memory storage as a placeholder storage["inmemory"] = configuration.Parameters{} // Disable upload purging // NOTE: Must use map[interface{}]interface{} for uploadpurging (not configuration.Parameters) // because distribution's validation code does a type assertion to map[interface{}]interface{} storage["maintenance"] = configuration.Parameters{ "uploadpurging": map[interface{}]interface{}{ "enabled": false, "age": 7 * 24 * time.Hour, // 168h "interval": 24 * time.Hour, // 24h "dryrun": false, }, } return storage } // buildMiddlewareConfig creates middleware configuration func buildMiddlewareConfig(defaultHold string) map[string][]configuration.Middleware { return map[string][]configuration.Middleware{ "registry": { { Name: "atproto-resolver", Options: configuration.Parameters{ "default_storage_endpoint": defaultHold, }, }, }, } } // buildAuthConfig creates authentication configuration from environment variables func buildAuthConfig(baseURL string) (configuration.Auth, error) { // Token configuration privateKeyPath := getEnvOrDefault("ATCR_AUTH_KEY_PATH", "/var/lib/atcr/auth/private-key.pem") certPath := getEnvOrDefault("ATCR_AUTH_CERT_PATH", "/var/lib/atcr/auth/private-key.crt") // Token expiration in seconds (default: 5 minutes) expirationStr := getEnvOrDefault("ATCR_TOKEN_EXPIRATION", "300") expiration, err := strconv.Atoi(expirationStr) if err != nil { return configuration.Auth{}, fmt.Errorf("invalid ATCR_TOKEN_EXPIRATION: %w", err) } // Auto-derive service name from base URL or use env var serviceName := getServiceName(baseURL) // Auto-derive realm from base URL realm := baseURL + "/auth/token" return configuration.Auth{ "token": configuration.Parameters{ "realm": realm, "service": serviceName, "issuer": serviceName, "rootcertbundle": certPath, "privatekey": privateKeyPath, "expiration": expiration, }, }, nil } // buildHealthConfig creates health check configuration func buildHealthConfig() configuration.Health { return configuration.Health{ StorageDriver: configuration.StorageDriver{ Enabled: true, Interval: 10 * time.Second, Threshold: 3, }, } } // getBaseURL determines the base URL for the service // Priority: ATCR_BASE_URL env var, then derived from HTTP addr func getBaseURL(httpAddr string) string { baseURL := os.Getenv("ATCR_BASE_URL") if baseURL != "" { return baseURL } // Auto-detect from HTTP addr if httpAddr[0] == ':' { // Just a port, assume localhost return fmt.Sprintf("http://127.0.0.1%s", httpAddr) } // Full address provided return fmt.Sprintf("http://%s", httpAddr) } // getServiceName extracts service name from base URL or uses env var func getServiceName(baseURL string) string { // Check env var first if serviceName := os.Getenv("ATCR_SERVICE_NAME"); serviceName != "" { return serviceName } // Try to extract from base URL parsed, err := url.Parse(baseURL) if err == nil && parsed.Hostname() != "" { hostname := parsed.Hostname() // Strip localhost/127.0.0.1 and use default if hostname == "localhost" || hostname == "127.0.0.1" { return "atcr.io" } return hostname } // Default fallback return "atcr.io" } // getEnvOrDefault gets an environment variable or returns a default value func getEnvOrDefault(key, defaultValue string) string { if val := os.Getenv(key); val != "" { return val } return defaultValue }