mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 17:24:16 +00:00
refactor appview to use envvars. move distribution configurations to code
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
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
|
||||
}
|
||||
+12
-15
@@ -60,11 +60,14 @@ func readOnlyAuthorizerCallback(action int, arg1, arg2, dbName string) int {
|
||||
}
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve <config>",
|
||||
Use: "serve",
|
||||
Short: "Start the ATCR registry server",
|
||||
Long: "Start the ATCR registry server with authentication endpoints",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: serveRegistry,
|
||||
Long: `Start the ATCR registry server with authentication endpoints.
|
||||
|
||||
Configuration is loaded from environment variables.
|
||||
See .env.appview.example for available environment variables.`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: serveRegistry,
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -87,19 +90,13 @@ func init() {
|
||||
}
|
||||
|
||||
func serveRegistry(cmd *cobra.Command, args []string) error {
|
||||
configPath := args[0]
|
||||
|
||||
// Parse configuration
|
||||
fp, err := os.Open(configPath)
|
||||
// Load configuration from environment variables
|
||||
fmt.Println("Loading configuration from environment variables...")
|
||||
config, err := loadConfigFromEnv()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open config file: %w", err)
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
config, err := configuration.Parse(fp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse configuration: %w", err)
|
||||
return fmt.Errorf("failed to load config from environment: %w", err)
|
||||
}
|
||||
fmt.Println("Configuration loaded successfully from environment")
|
||||
|
||||
// Initialize UI database first (required for all stores)
|
||||
fmt.Println("Initializing UI database...")
|
||||
|
||||
Reference in New Issue
Block a user