A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
80
fork

Configure Feed

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

begin support for helm-charts

+162 -63
+22 -17
pkg/appview/db/models.go
··· 22 22 MediaType string 23 23 ConfigDigest string 24 24 ConfigSize int64 25 + ArtifactType string // container-image, helm-chart, unknown 25 26 CreatedAt time.Time 26 27 // Annotations removed - now stored in repository_annotations table 27 28 } ··· 75 76 CreatedAt time.Time 76 77 HoldEndpoint string // Hold endpoint for health checking 77 78 Reachable bool // Whether the hold endpoint is reachable 79 + ArtifactType string // container-image, helm-chart, unknown 78 80 } 79 81 80 82 // Repository represents an aggregated view of a user's repository ··· 108 110 109 111 // FeaturedRepository represents a repository in the featured section 110 112 type FeaturedRepository struct { 111 - OwnerDID string 112 - OwnerHandle string 113 - Repository string 114 - Title string 115 - Description string 116 - IconURL string 117 - StarCount int 118 - PullCount int 119 - IsStarred bool // Whether the current user has starred this repository 113 + OwnerDID string 114 + OwnerHandle string 115 + Repository string 116 + Title string 117 + Description string 118 + IconURL string 119 + StarCount int 120 + PullCount int 121 + IsStarred bool // Whether the current user has starred this repository 122 + ArtifactType string // container-image, helm-chart, unknown 120 123 } 121 124 122 125 // RepositoryWithStats combines repository data with statistics ··· 127 130 128 131 // RepoCardData contains all data needed to render a repository card 129 132 type RepoCardData struct { 130 - OwnerHandle string 131 - Repository string 132 - Title string 133 - Description string 134 - IconURL string 135 - StarCount int 136 - PullCount int 137 - IsStarred bool // Whether the current user has starred this repository 133 + OwnerHandle string 134 + Repository string 135 + Title string 136 + Description string 137 + IconURL string 138 + StarCount int 139 + PullCount int 140 + IsStarred bool // Whether the current user has starred this repository 141 + ArtifactType string // container-image, helm-chart, unknown 138 142 } 139 143 140 144 // PlatformInfo represents platform information (OS/Architecture) ··· 163 167 HasAttestations bool // true if manifest list contains attestation references 164 168 Reachable bool // Whether the hold endpoint is reachable 165 169 Pending bool // Whether health check is still in progress 170 + // Note: ArtifactType is available via embedded Manifest struct 166 171 }
+39 -18
pkg/appview/db/queries.go
··· 13 13 return fmt.Sprintf("https://imgs.blue/%s/%s", did, cid) 14 14 } 15 15 16 + // GetArtifactType determines the artifact type based on config media type 17 + // Returns: "helm-chart", "container-image", or "unknown" 18 + func GetArtifactType(configMediaType string) string { 19 + switch { 20 + case strings.Contains(configMediaType, "helm.config"): 21 + return "helm-chart" 22 + case strings.Contains(configMediaType, "oci.image.config") || 23 + strings.Contains(configMediaType, "docker.container.image"): 24 + return "container-image" 25 + case configMediaType == "": 26 + // Manifest lists don't have a config - treat as container-image 27 + return "container-image" 28 + default: 29 + return "unknown" 30 + } 31 + } 32 + 16 33 // escapeLikePattern escapes SQL LIKE wildcards (%, _) and backslash for safe searching. 17 34 // It also sanitizes the input to prevent injection attacks via special characters. 18 35 func escapeLikePattern(s string) string { ··· 53 70 COALESCE((SELECT COUNT(*) FROM stars WHERE starrer_did = ? AND owner_did = u.did AND repository = t.repository), 0), 54 71 t.created_at, 55 72 m.hold_endpoint, 56 - COALESCE(rp.avatar_cid, '') 73 + COALESCE(rp.avatar_cid, ''), 74 + COALESCE(m.artifact_type, 'container-image') 57 75 FROM tags t 58 76 JOIN users u ON t.did = u.did 59 77 JOIN manifests m ON t.did = m.did AND t.repository = m.repository AND t.digest = m.digest ··· 82 100 var p Push 83 101 var isStarredInt int 84 102 var avatarCID string 85 - if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.Title, &p.Description, &p.IconURL, &p.PullCount, &p.StarCount, &isStarredInt, &p.CreatedAt, &p.HoldEndpoint, &avatarCID); err != nil { 103 + if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.Title, &p.Description, &p.IconURL, &p.PullCount, &p.StarCount, &isStarredInt, &p.CreatedAt, &p.HoldEndpoint, &avatarCID, &p.ArtifactType); err != nil { 86 104 return nil, 0, err 87 105 } 88 106 p.IsStarred = isStarredInt > 0 ··· 133 151 COALESCE((SELECT COUNT(*) FROM stars WHERE starrer_did = ? AND owner_did = u.did AND repository = t.repository), 0), 134 152 t.created_at, 135 153 m.hold_endpoint, 136 - COALESCE(rp.avatar_cid, '') 154 + COALESCE(rp.avatar_cid, ''), 155 + COALESCE(m.artifact_type, 'container-image') 137 156 FROM tags t 138 157 JOIN users u ON t.did = u.did 139 158 JOIN manifests m ON t.did = m.did AND t.repository = m.repository AND t.digest = m.digest ··· 162 181 var p Push 163 182 var isStarredInt int 164 183 var avatarCID string 165 - if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.Title, &p.Description, &p.IconURL, &p.PullCount, &p.StarCount, &isStarredInt, &p.CreatedAt, &p.HoldEndpoint, &avatarCID); err != nil { 184 + if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.Title, &p.Description, &p.IconURL, &p.PullCount, &p.StarCount, &isStarredInt, &p.CreatedAt, &p.HoldEndpoint, &avatarCID, &p.ArtifactType); err != nil { 166 185 return nil, 0, err 167 186 } 168 187 p.IsStarred = isStarredInt > 0 ··· 274 293 // Get manifests for this repo 275 294 manifestRows, err := db.Query(` 276 295 SELECT id, digest, hold_endpoint, schema_version, media_type, 277 - config_digest, config_size, created_at 296 + config_digest, config_size, artifact_type, created_at 278 297 FROM manifests 279 298 WHERE did = ? AND repository = ? 280 299 ORDER BY created_at DESC ··· 290 309 m.Repository = r.Name 291 310 292 311 if err := manifestRows.Scan(&m.ID, &m.Digest, &m.HoldEndpoint, &m.SchemaVersion, 293 - &m.MediaType, &m.ConfigDigest, &m.ConfigSize, &m.CreatedAt); err != nil { 312 + &m.MediaType, &m.ConfigDigest, &m.ConfigSize, &m.ArtifactType, &m.CreatedAt); err != nil { 294 313 manifestRows.Close() 295 314 return nil, err 296 315 } ··· 560 579 _, err := db.Exec(` 561 580 INSERT INTO manifests 562 581 (did, repository, digest, hold_endpoint, schema_version, media_type, 563 - config_digest, config_size, created_at) 564 - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 582 + config_digest, config_size, artifact_type, created_at) 583 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 565 584 ON CONFLICT(did, repository, digest) DO UPDATE SET 566 585 hold_endpoint = excluded.hold_endpoint, 567 586 schema_version = excluded.schema_version, 568 587 media_type = excluded.media_type, 569 588 config_digest = excluded.config_digest, 570 - config_size = excluded.config_size 589 + config_size = excluded.config_size, 590 + artifact_type = excluded.artifact_type 571 591 `, manifest.DID, manifest.Repository, manifest.Digest, manifest.HoldEndpoint, 572 592 manifest.SchemaVersion, manifest.MediaType, manifest.ConfigDigest, 573 - manifest.ConfigSize, manifest.CreatedAt) 593 + manifest.ConfigSize, manifest.ArtifactType, manifest.CreatedAt) 574 594 575 595 if err != nil { 576 596 return 0, err ··· 931 951 SELECT 932 952 m.id, m.did, m.repository, m.digest, m.media_type, 933 953 m.schema_version, m.created_at, 934 - m.config_digest, m.config_size, m.hold_endpoint, 954 + m.config_digest, m.config_size, m.hold_endpoint, m.artifact_type, 935 955 GROUP_CONCAT(DISTINCT t.tag) as tags, 936 956 COUNT(DISTINCT mr.digest) as platform_count 937 957 FROM manifests m ··· 964 984 if err := rows.Scan( 965 985 &m.ID, &m.DID, &m.Repository, &m.Digest, &m.MediaType, 966 986 &m.SchemaVersion, &m.CreatedAt, 967 - &configDigest, &configSize, &m.HoldEndpoint, 987 + &configDigest, &configSize, &m.HoldEndpoint, &m.ArtifactType, 968 988 &tags, &m.PlatformCount, 969 989 ); err != nil { 970 990 return nil, err ··· 1062 1082 SELECT 1063 1083 m.id, m.did, m.repository, m.digest, m.media_type, 1064 1084 m.schema_version, m.created_at, 1065 - m.config_digest, m.config_size, m.hold_endpoint, 1085 + m.config_digest, m.config_size, m.hold_endpoint, m.artifact_type, 1066 1086 GROUP_CONCAT(DISTINCT t.tag) as tags 1067 1087 FROM manifests m 1068 1088 LEFT JOIN tags t ON m.digest = t.digest AND m.did = t.did AND m.repository = t.repository ··· 1071 1091 `, did, repository, digest).Scan( 1072 1092 &m.ID, &m.DID, &m.Repository, &m.Digest, &m.MediaType, 1073 1093 &m.SchemaVersion, &m.CreatedAt, 1074 - &configDigest, &configSize, &m.HoldEndpoint, 1094 + &configDigest, &configSize, &m.HoldEndpoint, &m.ArtifactType, 1075 1095 &tags, 1076 1096 ) 1077 1097 ··· 1374 1394 // Get manifests for this repo 1375 1395 manifestRows, err := db.Query(` 1376 1396 SELECT id, digest, hold_endpoint, schema_version, media_type, 1377 - config_digest, config_size, created_at 1397 + config_digest, config_size, artifact_type, created_at 1378 1398 FROM manifests 1379 1399 WHERE did = ? AND repository = ? 1380 1400 ORDER BY created_at DESC ··· 1390 1410 m.Repository = repository 1391 1411 1392 1412 if err := manifestRows.Scan(&m.ID, &m.Digest, &m.HoldEndpoint, &m.SchemaVersion, 1393 - &m.MediaType, &m.ConfigDigest, &m.ConfigSize, &m.CreatedAt); err != nil { 1413 + &m.MediaType, &m.ConfigDigest, &m.ConfigSize, &m.ArtifactType, &m.CreatedAt); err != nil { 1394 1414 manifestRows.Close() 1395 1415 return nil, err 1396 1416 } ··· 1675 1695 rs.pull_count, 1676 1696 rs.star_count, 1677 1697 COALESCE((SELECT COUNT(*) FROM stars WHERE starrer_did = ? AND owner_did = m.did AND repository = m.repository), 0), 1678 - COALESCE(rp.avatar_cid, '') 1698 + COALESCE(rp.avatar_cid, ''), 1699 + COALESCE(m.artifact_type, 'container-image') 1679 1700 FROM latest_manifests lm 1680 1701 JOIN manifests m ON lm.latest_id = m.id 1681 1702 JOIN users u ON m.did = u.did ··· 1698 1719 var avatarCID string 1699 1720 1700 1721 if err := rows.Scan(&f.OwnerDID, &f.OwnerHandle, &f.Repository, 1701 - &f.Title, &f.Description, &f.IconURL, &f.PullCount, &f.StarCount, &isStarredInt, &avatarCID); err != nil { 1722 + &f.Title, &f.Description, &f.IconURL, &f.PullCount, &f.StarCount, &isStarredInt, &avatarCID, &f.ArtifactType); err != nil { 1702 1723 return nil, err 1703 1724 } 1704 1725 f.IsStarred = isStarredInt > 0
+2
pkg/appview/db/schema.sql
··· 27 27 media_type TEXT NOT NULL, 28 28 config_digest TEXT, 29 29 config_size INTEGER, 30 + artifact_type TEXT NOT NULL DEFAULT 'container-image', -- container-image, helm-chart, unknown 30 31 created_at TIMESTAMP NOT NULL, 31 32 UNIQUE(did, repository, digest), 32 33 FOREIGN KEY(did) REFERENCES users(did) ON DELETE CASCADE ··· 34 35 CREATE INDEX IF NOT EXISTS idx_manifests_did_repo ON manifests(did, repository); 35 36 CREATE INDEX IF NOT EXISTS idx_manifests_created_at ON manifests(created_at DESC); 36 37 CREATE INDEX IF NOT EXISTS idx_manifests_digest ON manifests(digest); 38 + CREATE INDEX IF NOT EXISTS idx_manifests_artifact_type ON manifests(artifact_type); 37 39 38 40 CREATE TABLE IF NOT EXISTS repository_annotations ( 39 41 did TEXT NOT NULL,
+9 -8
pkg/appview/handlers/home.go
··· 39 39 cards := make([]db.RepoCardData, len(featured)) 40 40 for i, repo := range featured { 41 41 cards[i] = db.RepoCardData{ 42 - OwnerHandle: repo.OwnerHandle, 43 - Repository: repo.Repository, 44 - Title: repo.Title, 45 - Description: repo.Description, 46 - IconURL: repo.IconURL, 47 - StarCount: repo.StarCount, 48 - PullCount: repo.PullCount, 49 - IsStarred: repo.IsStarred, 42 + OwnerHandle: repo.OwnerHandle, 43 + Repository: repo.Repository, 44 + Title: repo.Title, 45 + Description: repo.Description, 46 + IconURL: repo.IconURL, 47 + StarCount: repo.StarCount, 48 + PullCount: repo.PullCount, 49 + IsStarred: repo.IsStarred, 50 + ArtifactType: repo.ArtifactType, 50 51 } 51 52 } 52 53
+26 -17
pkg/appview/handlers/repository.go
··· 231 231 } 232 232 } 233 233 234 + // Determine dominant artifact type from manifests 235 + artifactType := "container-image" 236 + if len(manifests) > 0 { 237 + // Use the most recent manifest's artifact type 238 + artifactType = manifests[0].ArtifactType 239 + } 240 + 234 241 data := struct { 235 242 PageData 236 - Owner *db.User // Repository owner 237 - Repository *db.Repository // Repository summary 238 - Tags []db.TagWithPlatforms // Tags with platform info 239 - Manifests []db.ManifestWithMetadata // Top-level manifests only 240 - StarCount int 241 - IsStarred bool 242 - IsOwner bool // Whether current user owns this repository 243 - ReadmeHTML template.HTML 243 + Owner *db.User // Repository owner 244 + Repository *db.Repository // Repository summary 245 + Tags []db.TagWithPlatforms // Tags with platform info 246 + Manifests []db.ManifestWithMetadata // Top-level manifests only 247 + StarCount int 248 + IsStarred bool 249 + IsOwner bool // Whether current user owns this repository 250 + ReadmeHTML template.HTML 251 + ArtifactType string // Dominant artifact type: container-image, helm-chart, unknown 244 252 }{ 245 - PageData: NewPageData(r, h.RegistryURL), 246 - Owner: owner, 247 - Repository: repo, 248 - Tags: tagsWithPlatforms, 249 - Manifests: manifests, 250 - StarCount: stats.StarCount, 251 - IsStarred: isStarred, 252 - IsOwner: isOwner, 253 - ReadmeHTML: readmeHTML, 253 + PageData: NewPageData(r, h.RegistryURL), 254 + Owner: owner, 255 + Repository: repo, 256 + Tags: tagsWithPlatforms, 257 + Manifests: manifests, 258 + StarCount: stats.StarCount, 259 + IsStarred: isStarred, 260 + IsOwner: isOwner, 261 + ReadmeHTML: readmeHTML, 262 + ArtifactType: artifactType, 254 263 } 255 264 256 265 if err := h.Templates.ExecuteTemplate(w, "repository", data); err != nil {
+1 -1
pkg/appview/jetstream/backfill.go
··· 49 49 50 50 return &BackfillWorker{ 51 51 db: database, 52 - client: client, // This points to the relay 52 + client: client, // This points to the relay 53 53 processor: NewProcessor(database, false, NewStatsCache()), // Stats cache for aggregation 54 54 defaultHoldDID: defaultHoldDID, 55 55 testMode: testMode,
+7
pkg/appview/jetstream/processor.go
··· 119 119 holdDID = atproto.ResolveHoldDIDFromURL(manifestRecord.HoldEndpoint) 120 120 } 121 121 122 + // Detect artifact type from config media type 123 + artifactType := "container-image" 124 + if !isManifestList && manifestRecord.Config != nil { 125 + artifactType = db.GetArtifactType(manifestRecord.Config.MediaType) 126 + } 127 + 122 128 // Prepare manifest for insertion (WITHOUT annotation fields) 123 129 manifest := &db.Manifest{ 124 130 DID: did, ··· 127 133 MediaType: manifestRecord.MediaType, 128 134 SchemaVersion: manifestRecord.SchemaVersion, 129 135 HoldEndpoint: holdDID, 136 + ArtifactType: artifactType, 130 137 CreatedAt: manifestRecord.CreatedAt, 131 138 // Annotations removed - stored separately in repository_annotations table 132 139 }
+28
pkg/appview/static/css/style.css
··· 2429 2429 font-size: 1.5rem; 2430 2430 } 2431 2431 } 2432 + 2433 + /* Artifact type badges */ 2434 + .artifact-badge { 2435 + display: inline-flex; 2436 + align-items: center; 2437 + justify-content: center; 2438 + padding: 0.15rem 0.35rem; 2439 + border-radius: 4px; 2440 + font-size: 0.7rem; 2441 + font-weight: 500; 2442 + margin-left: 0.5rem; 2443 + vertical-align: middle; 2444 + } 2445 + 2446 + .artifact-badge.helm { 2447 + background-color: rgba(13, 108, 191, 0.15); 2448 + color: #0d6cbf; 2449 + } 2450 + 2451 + .artifact-badge i { 2452 + width: 12px; 2453 + height: 12px; 2454 + } 2455 + 2456 + .manifest-type.helm { 2457 + background-color: rgba(13, 108, 191, 0.15); 2458 + color: #0d6cbf; 2459 + }
+4
pkg/appview/templates/components/repo-card.html
··· 10 10 - IconURL: string (optional) - Repository icon URL 11 11 - StarCount: int - Number of stars 12 12 - PullCount: int - Number of pulls 13 + - ArtifactType: string - container-image, helm-chart, unknown 13 14 */}} 14 15 <a href="/r/{{ .OwnerHandle }}/{{ .Repository }}" class="featured-card"> 15 16 <div class="featured-header"> ··· 23 24 <span class="featured-owner">{{ .OwnerHandle }}</span> 24 25 <span class="featured-separator">/</span> 25 26 <span class="featured-name">{{ .Repository }}</span> 27 + {{ if eq .ArtifactType "helm-chart" }} 28 + <span class="artifact-badge helm"><i data-lucide="anchor"></i></span> 29 + {{ end }} 26 30 </div> 27 31 {{ if .Description }} 28 32 <p class="featured-description">{{ .Description }}</p>
+18 -2
pkg/appview/templates/pages/repository.html
··· 101 101 102 102 <!-- Pull Command --> 103 103 <div class="pull-command-section"> 104 + {{ if eq .ArtifactType "helm-chart" }} 105 + <h3>Pull this chart</h3> 106 + {{ if .Tags }} 107 + {{ $firstTag := index .Tags 0 }} 108 + {{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name " --version " $firstTag.Tag.Tag) }} 109 + {{ else }} 110 + {{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name) }} 111 + {{ end }} 112 + {{ else }} 104 113 <h3>Pull this image</h3> 105 114 {{ if .Tags }} 106 115 {{ $firstTag := index .Tags 0 }} 107 116 {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" $firstTag.Tag.Tag) }} 108 117 {{ else }} 109 118 {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":latest") }} 119 + {{ end }} 110 120 {{ end }} 111 121 </div> 112 122 </div> ··· 173 183 {{ end }} 174 184 </div> 175 185 </div> 176 - {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" .Tag.Tag) }} 186 + {{ if eq $.ArtifactType "helm-chart" }} 187 + {{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name " --version " .Tag.Tag) }} 188 + {{ else }} 189 + {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" .Tag.Tag) }} 190 + {{ end }} 177 191 </div> 178 192 {{ end }} 179 193 </div> ··· 199 213 <div> 200 214 {{ if .IsManifestList }} 201 215 <span class="manifest-type"><i data-lucide="package"></i> Multi-arch</span> 216 + {{ else if eq .ArtifactType "helm-chart" }} 217 + <span class="manifest-type helm"><i data-lucide="anchor"></i> Helm Chart</span> 202 218 {{ else }} 203 - <span class="manifest-type"><i data-lucide="file-text"></i> Image</span> 219 + <span class="manifest-type"><i data-lucide="box"></i> Image</span> 204 220 {{ end }} 205 221 {{ if .HasAttestations }} 206 222 <span class="badge-attestation"><i data-lucide="shield-check"></i> Attestations</span>
+3
pkg/appview/templates/partials/push-list.html
··· 14 14 <a href="/r/{{ .Handle }}/{{ .Repository }}" class="push-repo">{{ .Repository }}</a> 15 15 <span class="push-separator">:</span> 16 16 <span class="push-tag">{{ .Tag }}</span> 17 + {{ if eq .ArtifactType "helm-chart" }} 18 + <span class="artifact-badge helm"><i data-lucide="anchor"></i></span> 19 + {{ end }} 17 20 </div> 18 21 <div class="push-stats"> 19 22 <span class="push-stat">
+3
pkg/auth/oauth/client.go
··· 93 93 "blob:application/vnd.docker.distribution.manifest.list.v2+json", 94 94 // OCI artifact manifests (for cosign signatures, SBOMs, attestations) 95 95 "blob:application/vnd.cncf.oras.artifact.manifest.v1+json", 96 + // Helm chart support 97 + "blob:application/vnd.cncf.helm.config.v1+json", 98 + "blob:application/vnd.cncf.helm.chart.content.v1.tar+gzip", 96 99 // Image avatars 97 100 "blob:image/*", 98 101 }