A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

begin moving to slog

+138 -33
+4
cmd/appview/serve.go
··· 25 25 "atcr.io/pkg/auth" 26 26 "atcr.io/pkg/auth/oauth" 27 27 "atcr.io/pkg/auth/token" 28 + "atcr.io/pkg/logging" 28 29 29 30 // UI components 30 31 "atcr.io/pkg/appview" ··· 58 59 } 59 60 60 61 func serveRegistry(cmd *cobra.Command, args []string) error { 62 + // Initialize structured logging 63 + logging.InitLogger(appview.GetLogLevel()) 64 + 61 65 // Load configuration from environment variables 62 66 fmt.Println("Loading configuration from environment variables...") 63 67 config, err := appview.LoadConfigFromEnv()
+36 -26
cmd/hold/main.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 - "log" 6 + "log/slog" 7 7 "net/http" 8 8 "os" 9 9 "os/signal" ··· 13 13 "atcr.io/pkg/hold" 14 14 "atcr.io/pkg/hold/oci" 15 15 "atcr.io/pkg/hold/pds" 16 + "atcr.io/pkg/logging" 16 17 "atcr.io/pkg/s3" 17 18 18 19 // Import storage drivers ··· 28 29 // Load configuration from environment variables 29 30 cfg, err := hold.LoadConfigFromEnv() 30 31 if err != nil { 31 - log.Fatalf("Failed to load config: %v", err) 32 + slog.Error("Failed to load config", "error", err) 33 + os.Exit(1) 32 34 } 33 35 36 + // Initialize structured logging 37 + logging.InitLogger(cfg.LogLevel) 38 + 34 39 // Initialize embedded PDS if database path is configured 35 40 // This must happen before creating HoldService since service needs PDS for authorization 36 41 var holdPDS *pds.HoldPDS ··· 39 44 if cfg.Database.Path != "" { 40 45 // Generate did:web from public URL 41 46 holdDID := pds.GenerateDIDFromURL(cfg.Server.PublicURL) 42 - log.Printf("Initializing embedded PDS with DID: %s", holdDID) 47 + slog.Info("Initializing embedded PDS", "did", holdDID) 43 48 44 49 // Initialize PDS with carstore and keys 45 50 ctx := context.Background() 46 51 holdPDS, err = pds.NewHoldPDS(ctx, holdDID, cfg.Server.PublicURL, cfg.Database.Path, cfg.Database.KeyPath, cfg.Registration.EnableBlueskyPosts) 47 52 if err != nil { 48 - log.Fatalf("Failed to initialize embedded PDS: %v", err) 53 + slog.Error("Failed to initialize embedded PDS", "error", err) 54 + os.Exit(1) 49 55 } 50 56 51 57 // Create storage driver from config (needed for bootstrap profile avatar) 52 58 driver, err := factory.Create(ctx, cfg.Storage.Type(), cfg.Storage.Parameters()) 53 59 if err != nil { 54 - log.Fatalf("failed to create storage driver: %v", err) 55 - return 60 + slog.Error("Failed to create storage driver", "error", err) 61 + os.Exit(1) 56 62 } 57 63 58 64 // Bootstrap PDS with captain record, hold owner as first crew member, and profile 59 65 if err := holdPDS.Bootstrap(ctx, driver, cfg.Registration.OwnerDID, cfg.Server.Public, cfg.Registration.AllowAllCrew, cfg.Registration.ProfileAvatarURL); err != nil { 60 - log.Fatalf("Failed to bootstrap PDS: %v", err) 66 + slog.Error("Failed to bootstrap PDS", "error", err) 67 + os.Exit(1) 61 68 } 62 69 63 70 // Create event broadcaster for subscribeRepos firehose ··· 72 79 73 80 // Bootstrap events from existing repo records (one-time migration) 74 81 if err := broadcaster.BootstrapFromRepo(holdPDS); err != nil { 75 - log.Printf("Warning: Failed to bootstrap events from repo: %v", err) 82 + slog.Warn("Failed to bootstrap events from repo", "error", err) 76 83 } 77 84 78 85 // Wire up repo event handler to broadcaster 79 86 holdPDS.RepomgrRef().SetEventHandler(broadcaster.SetRepoEventHandler(), true) 80 87 81 - log.Printf("Embedded PDS initialized successfully with firehose enabled") 88 + slog.Info("Embedded PDS initialized successfully with firehose enabled") 82 89 } else { 83 - log.Fatalf("Database path is required for embedded PDS authorization") 90 + slog.Error("Database path is required for embedded PDS authorization") 91 + os.Exit(1) 84 92 } 85 93 86 94 // Create blob store adapter and XRPC handlers ··· 90 98 ctx := context.Background() 91 99 driver, err := factory.Create(ctx, cfg.Storage.Type(), cfg.Storage.Parameters()) 92 100 if err != nil { 93 - log.Fatalf("failed to create storage driver: %v", err) 94 - return 101 + slog.Error("Failed to create storage driver", "error", err) 102 + os.Exit(1) 95 103 } 96 104 97 105 s3Service, err := s3.NewS3Service(cfg.Storage.Parameters(), cfg.Server.DisablePresignedURLs, cfg.Storage.Type()) 98 106 if err != nil { 99 - log.Fatalf("Failed to create s3 service: %v", err) 107 + slog.Error("Failed to create S3 service", "error", err) 108 + os.Exit(1) 100 109 } 101 110 102 111 // Create PDS XRPC handler (ATProto endpoints) ··· 128 137 129 138 // Register XRPC/ATProto PDS endpoints if PDS is initialized 130 139 if xrpcHandler != nil { 131 - log.Printf("Registering ATProto PDS endpoints") 140 + slog.Info("Registering ATProto PDS endpoints") 132 141 xrpcHandler.RegisterHandlers(r) 133 142 } 134 143 135 144 // Register OCI multipart upload endpoints 136 145 if ociHandler != nil { 137 - log.Printf("Registering OCI multipart upload endpoints") 146 + slog.Info("Registering OCI multipart upload endpoints") 138 147 ociHandler.RegisterHandlers(r) 139 148 } 140 149 ··· 153 162 // Start server in goroutine 154 163 serverErr := make(chan error, 1) 155 164 go func() { 156 - log.Printf("Starting hold service on %s", cfg.Server.Addr) 165 + slog.Info("Starting hold service", "addr", cfg.Server.Addr) 157 166 if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 158 167 serverErr <- err 159 168 } ··· 164 173 ctx := context.Background() 165 174 166 175 if err := holdPDS.SetStatus(ctx, "online"); err != nil { 167 - log.Printf("Warning: Failed to set status post to online: %v", err) 176 + slog.Warn("Failed to set status post to online", "error", err) 168 177 } else { 169 - log.Printf("Status post set to online") 178 + slog.Info("Status post set to online") 170 179 } 171 180 } 172 181 173 182 // Wait for signal or server error 174 183 select { 175 184 case err := <-serverErr: 176 - log.Fatalf("Server failed: %v", err) 185 + slog.Error("Server failed", "error", err) 186 + os.Exit(1) 177 187 case sig := <-sigChan: 178 - log.Printf("Received signal %v, shutting down gracefully...", sig) 188 + slog.Info("Received signal, shutting down gracefully", "signal", sig) 179 189 180 190 // Update status post to "offline" before shutdown 181 191 if holdPDS != nil { 182 192 ctx := context.Background() 183 193 if err := holdPDS.SetStatus(ctx, "offline"); err != nil { 184 - log.Printf("Warning: Failed to set status post to offline: %v", err) 194 + slog.Warn("Failed to set status post to offline", "error", err) 185 195 } else { 186 - log.Printf("Status post set to offline") 196 + slog.Info("Status post set to offline") 187 197 } 188 198 } 189 199 190 200 // Close broadcaster database connection 191 201 if broadcaster != nil { 192 202 if err := broadcaster.Close(); err != nil { 193 - log.Printf("Warning: Failed to close broadcaster database: %v", err) 203 + slog.Warn("Failed to close broadcaster database", "error", err) 194 204 } else { 195 - log.Printf("Broadcaster database closed") 205 + slog.Info("Broadcaster database closed") 196 206 } 197 207 } 198 208 ··· 201 211 defer cancel() 202 212 203 213 if err := server.Shutdown(shutdownCtx); err != nil { 204 - log.Printf("Server shutdown error: %v", err) 214 + slog.Error("Server shutdown error", "error", err) 205 215 } else { 206 - log.Printf("Server shutdown complete") 216 + slog.Info("Server shutdown complete") 207 217 } 208 218 } 209 219 }
+6
pkg/appview/config.go
··· 240 240 return defaultValue 241 241 } 242 242 243 + // GetLogLevel returns the configured log level from environment 244 + // Centralizes ATCR_LOG_LEVEL env var reading 245 + func GetLogLevel() string { 246 + return GetEnvOrDefault("ATCR_LOG_LEVEL", "info") 247 + } 248 + 243 249 // GetStringParam extracts a string parameter from configuration.Parameters 244 250 func GetStringParam(params configuration.Parameters, key, defaultValue string) string { 245 251 if v, ok := params[key]; ok {
+4
pkg/hold/config.go
··· 17 17 // Config represents the hold service configuration 18 18 type Config struct { 19 19 Version string `yaml:"version"` 20 + LogLevel string `yaml:"log_level"` 20 21 Storage StorageConfig `yaml:"storage"` 21 22 Server ServerConfig `yaml:"server"` 22 23 Registration RegistrationConfig `yaml:"registration"` ··· 89 90 cfg := &Config{ 90 91 Version: "0.1", 91 92 } 93 + 94 + // Logging configuration 95 + cfg.LogLevel = getEnvOrDefault("ATCR_LOG_LEVEL", "info") 92 96 93 97 // Server configuration 94 98 cfg.Server.Addr = getEnvOrDefault("HOLD_SERVER_ADDR", ":8080")
+58
pkg/logging/logger.go
··· 1 + // Package logging provides centralized structured logging using slog 2 + // with configurable log levels. Call InitLogger() from main() to configure. 3 + package logging 4 + 5 + import ( 6 + "io" 7 + "log/slog" 8 + "os" 9 + "strings" 10 + ) 11 + 12 + // InitLogger initializes the global slog default logger with the specified log level. 13 + // Valid levels: debug, info, warn, error (case-insensitive) 14 + // If level is empty or invalid, defaults to INFO. 15 + // Call this from main() at startup. 16 + func InitLogger(level string) { 17 + var logLevel slog.Level 18 + 19 + switch strings.ToLower(strings.TrimSpace(level)) { 20 + case "debug": 21 + logLevel = slog.LevelDebug 22 + case "info", "": 23 + logLevel = slog.LevelInfo 24 + case "warn", "warning": 25 + logLevel = slog.LevelWarn 26 + case "error": 27 + logLevel = slog.LevelError 28 + default: 29 + logLevel = slog.LevelInfo 30 + } 31 + 32 + opts := &slog.HandlerOptions{ 33 + Level: logLevel, 34 + } 35 + 36 + handler := slog.NewTextHandler(os.Stdout, opts) 37 + slog.SetDefault(slog.New(handler)) 38 + } 39 + 40 + // SetupTestLogger configures logging for tests to reduce noise. 41 + // Sets log level to WARN and outputs to io.Discard to suppress DEBUG and INFO messages. 42 + // Returns a cleanup function that should be called when the test completes (use t.Cleanup). 43 + func SetupTestLogger() func() { 44 + // Save original logger to restore later 45 + originalLogger := slog.Default() 46 + 47 + // Set level to WARN and discard output to silence tests 48 + opts := &slog.HandlerOptions{ 49 + Level: slog.LevelWarn, 50 + } 51 + handler := slog.NewTextHandler(io.Discard, opts) 52 + slog.SetDefault(slog.New(handler)) 53 + 54 + // Return cleanup function 55 + return func() { 56 + slog.SetDefault(originalLogger) 57 + } 58 + }
+10 -5
pkg/s3/types.go
··· 5 5 6 6 import ( 7 7 "fmt" 8 - "log" 8 + "log/slog" 9 9 "strings" 10 10 11 11 "github.com/aws/aws-sdk-go/aws" ··· 26 26 func NewS3Service(params map[string]any, disablePresigned bool, storageType string) (*S3Service, error) { 27 27 // Check if presigned URLs are explicitly disabled 28 28 if disablePresigned { 29 - log.Printf("⚠️ S3 presigned URLs DISABLED by config (DISABLE_PRESIGNED_URLS=true)") 30 - log.Printf(" All uploads will use buffered mode (parts buffered in hold service)") 29 + slog.Warn("S3 presigned URLs DISABLED by config", 30 + "reason", "DISABLE_PRESIGNED_URLS=true", 31 + "uploadMode", "buffered") 31 32 return &S3Service{}, nil 32 33 } 33 34 34 35 // Check if storage driver is S3 35 36 if storageType != "s3" { 36 - log.Printf("Storage driver is %s (not S3), presigned URLs disabled", storageType) 37 + slog.Info("Presigned URLs disabled for non-S3 storage", 38 + "storageDriver", storageType) 37 39 return &S3Service{}, nil 38 40 } 39 41 ··· 79 81 s3PathPrefix = strings.TrimPrefix(rootDir, "/") 80 82 } 81 83 82 - log.Printf("✅ S3 presigned URLs enabled") 84 + slog.Info("S3 presigned URLs enabled", 85 + "bucket", bucket, 86 + "region", region, 87 + "pathPrefix", s3PathPrefix) 83 88 84 89 // Create S3 client 85 90 return &S3Service{
+18
pkg/s3/types_test.go
··· 2 2 3 3 import ( 4 4 "testing" 5 + 6 + "atcr.io/pkg/logging" 5 7 ) 6 8 7 9 func TestNewS3Service_PresignedDisabled(t *testing.T) { 10 + t.Cleanup(logging.SetupTestLogger()) 11 + 8 12 params := map[string]any{ 9 13 "bucket": "test-bucket", 10 14 "region": "us-west-2", ··· 24 28 } 25 29 26 30 func TestNewS3Service_NonS3Storage(t *testing.T) { 31 + t.Cleanup(logging.SetupTestLogger()) 32 + 27 33 params := map[string]any{ 28 34 "rootdirectory": "/tmp/test", 29 35 } ··· 42 48 } 43 49 44 50 func TestNewS3Service_MissingBucket(t *testing.T) { 51 + t.Cleanup(logging.SetupTestLogger()) 52 + 45 53 params := map[string]any{ 46 54 "region": "us-east-1", 47 55 "accesskey": "test-key", ··· 56 64 } 57 65 58 66 func TestNewS3Service_Success(t *testing.T) { 67 + t.Cleanup(logging.SetupTestLogger()) 68 + 59 69 params := map[string]any{ 60 70 "bucket": "test-bucket", 61 71 "region": "us-west-2", ··· 80 90 } 81 91 82 92 func TestNewS3Service_WithEndpoint(t *testing.T) { 93 + t.Cleanup(logging.SetupTestLogger()) 94 + 83 95 params := map[string]any{ 84 96 "bucket": "test-bucket", 85 97 "region": "us-east-1", ··· 102 114 } 103 115 104 116 func TestNewS3Service_DefaultRegion(t *testing.T) { 117 + t.Cleanup(logging.SetupTestLogger()) 118 + 105 119 params := map[string]any{ 106 120 "bucket": "test-bucket", 107 121 "accesskey": "test-key", ··· 123 137 } 124 138 125 139 func TestNewS3Service_WithPathPrefix(t *testing.T) { 140 + t.Cleanup(logging.SetupTestLogger()) 141 + 126 142 params := map[string]any{ 127 143 "bucket": "test-bucket", 128 144 "region": "us-east-1", ··· 142 158 } 143 159 144 160 func TestNewS3Service_NoCredentials(t *testing.T) { 161 + t.Cleanup(logging.SetupTestLogger()) 162 + 145 163 params := map[string]any{ 146 164 "bucket": "test-bucket", 147 165 "region": "us-east-1",
+2 -2
scripts/migrate-image.sh
··· 109 109 echo "Found multi-arch manifest list" 110 110 echo "" 111 111 112 - # Extract platform information and digests 113 - PLATFORMS=$(echo "$MANIFEST_JSON" | jq -r '.manifests[] | "\(.platform.os)|\(.platform.architecture)|\(.platform.variant // "")|\(.digest)"') 112 + # Extract platform information and digests (skip unknown/unknown for cosign artifacts) 113 + PLATFORMS=$(echo "$MANIFEST_JSON" | jq -r '.manifests[] | select(.platform.os != "unknown" and .platform.architecture != "unknown") | "\(.platform.os)|\(.platform.architecture)|\(.platform.variant // "")|\(.digest)"') 114 114 115 115 # Arrays to store pushed images for manifest creation 116 116 declare -a PUSHED_IMAGES