Files
at-container-registry/cmd/appview/config.go
2025-10-10 09:43:26 -05:00

214 lines
5.8 KiB
Go

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
}