A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
1// Package s3 provides S3 client initialization and presigned URL generation
2// for hold services. It supports S3, Storj, and Minio storage backends,
3// with fallback to buffered proxy mode when presigned URLs are unavailable.
4package s3
5
6import (
7 "fmt"
8 "log/slog"
9 "strings"
10
11 "github.com/aws/aws-sdk-go/aws"
12 "github.com/aws/aws-sdk-go/aws/credentials"
13 "github.com/aws/aws-sdk-go/aws/session"
14 "github.com/aws/aws-sdk-go/service/s3"
15)
16
17type S3Service struct {
18 Client *s3.S3 // S3 client for presigned URLs (nil if not S3 storage)
19 Bucket string // S3 bucket name
20 PathPrefix string // S3 path prefix (if any)
21}
22
23// NewS3Service initializes the S3 client for presigned URL generation
24// Returns nil error if S3 client is successfully initialized
25// Returns error if storage is not S3 or if initialization fails (service will fall back to proxy mode)
26func NewS3Service(params map[string]any, disablePresigned bool, storageType string) (*S3Service, error) {
27 // Check if presigned URLs are explicitly disabled
28 if disablePresigned {
29 slog.Warn("S3 presigned URLs DISABLED by config",
30 "reason", "DISABLE_PRESIGNED_URLS=true",
31 "uploadMode", "buffered")
32 return &S3Service{}, nil
33 }
34
35 // Check if storage driver is S3
36 if storageType != "s3" {
37 slog.Info("Presigned URLs disabled for non-S3 storage",
38 "storageDriver", storageType)
39 return &S3Service{}, nil
40 }
41
42 // Extract required S3 configuration
43 region, _ := params["region"].(string)
44 if region == "" {
45 region = "us-east-1" // Default region
46 }
47
48 accessKey, _ := params["accesskey"].(string)
49 secretKey, _ := params["secretkey"].(string)
50 bucket, _ := params["bucket"].(string)
51
52 if bucket == "" {
53 return nil, fmt.Errorf("S3 bucket not configured")
54 }
55
56 // Build AWS config
57 awsConfig := &aws.Config{
58 Region: ®ion,
59 }
60
61 // Add credentials if provided (allow IAM role auth if not provided)
62 if accessKey != "" && secretKey != "" {
63 awsConfig.Credentials = credentials.NewStaticCredentials(accessKey, secretKey, "")
64 }
65
66 // Add custom endpoint for S3-compatible services (Storj, MinIO, R2, etc.)
67 if endpoint, ok := params["regionendpoint"].(string); ok && endpoint != "" {
68 awsConfig.Endpoint = &endpoint
69 awsConfig.S3ForcePathStyle = aws.Bool(true) // Required for MinIO, Storj
70 }
71
72 // Create AWS session
73 sess, err := session.NewSession(awsConfig)
74 if err != nil {
75 return nil, fmt.Errorf("failed to create AWS session: %w", err)
76 }
77
78 var s3PathPrefix string
79 // Extract path prefix if configured (rootdirectory in S3 params)
80 if rootDir, ok := params["rootdirectory"].(string); ok && rootDir != "" {
81 s3PathPrefix = strings.TrimPrefix(rootDir, "/")
82 }
83
84 slog.Info("S3 presigned URLs enabled",
85 "bucket", bucket,
86 "region", region,
87 "pathPrefix", s3PathPrefix)
88
89 // Create S3 client
90 return &S3Service{
91 Client: s3.New(sess),
92 Bucket: bucket,
93 PathPrefix: s3PathPrefix,
94 }, nil
95}
96
97// BlobPath converts a digest (e.g., "sha256:abc123...") or temp path to a storage path
98// Distribution stores blobs as: /docker/registry/v2/blobs/{algorithm}/{xx}/{hash}/data
99// where xx is the first 2 characters of the hash for directory sharding
100// NOTE: Path must start with / for filesystem driver
101// This is used for OCI container layers (content-addressed, globally deduplicated)
102func BlobPath(digest string) string {
103 // Handle temp paths (start with uploads/temp-)
104 if strings.HasPrefix(digest, "uploads/temp-") {
105 return fmt.Sprintf("/docker/registry/v2/%s/data", digest)
106 }
107
108 // Split digest into algorithm and hash
109 parts := strings.SplitN(digest, ":", 2)
110 if len(parts) != 2 {
111 // Fallback for malformed digest
112 return fmt.Sprintf("/docker/registry/v2/blobs/%s/data", digest)
113 }
114
115 algorithm := parts[0]
116 hash := parts[1]
117
118 // Use first 2 characters for sharding
119 if len(hash) < 2 {
120 return fmt.Sprintf("/docker/registry/v2/blobs/%s/%s/data", algorithm, hash)
121 }
122
123 return fmt.Sprintf("/docker/registry/v2/blobs/%s/%s/%s/data", algorithm, hash[:2], hash)
124}