mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-05-02 14:15:51 +00:00
214 lines
5.8 KiB
Go
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
|
|
}
|