A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
81
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