tangled
alpha
login
or
join now
evan.jarrett.net
/
at-container-registry
66
fork
atom
A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
66
fork
atom
overview
issues
1
pulls
pipelines
fix issue with mismatched scopes locally
evan.jarrett.net
4 months ago
1f72d907
abf48407
verified
This commit was signed with the committer's
known signature
.
evan.jarrett.net
SSH Key Fingerprint:
SHA256:bznk0uVPp7XFOl67P0uTM1pCjf2A4ojeP/lsUE7uauQ=
1/1
tests.yml
success
1min 2s
+145
-21
8 changed files
expand all
collapse all
unified
split
Dockerfile.appview
cmd
appview
serve.go
docs
appview.md
go.mod
pkg
appview
readme
fetcher.go
auth
oauth
client.go
interactive.go
scripts
test-e2e.sh
+2
-1
Dockerfile.appview
···
39
39
org.opencontainers.image.documentation="https://tangled.org/@evan.jarrett.net/at-container-registry" \
40
40
org.opencontainers.image.licenses="MIT" \
41
41
org.opencontainers.image.version="0.1.0" \
42
42
-
io.atcr.icon="https://imgs.blue/evan.jarrett.net/1TpTNrRelfloN2emuWZDrWmPT0o93bAjEnozjD6UPgoVV9m4"
42
42
+
io.atcr.icon="https://imgs.blue/evan.jarrett.net/1TpTNrRelfloN2emuWZDrWmPT0o93bAjEnozjD6UPgoVV9m4" \
43
43
+
io.atcr.readme="https://tangled.org/@evan.jarrett.net/at-container-registry/raw/main/docs/appview.md"
43
44
44
45
ENTRYPOINT ["/atcr-appview"]
45
46
CMD ["serve"]
+13
-9
cmd/appview/serve.go
···
154
154
// The extraction function normalizes URLs to DIDs for consistency
155
155
defaultHoldDID := appview.ExtractDefaultHoldDID(config)
156
156
157
157
+
// Extract test mode from config (needed for OAuth scope configuration)
158
158
+
testMode := appview.ExtractTestMode(config)
159
159
+
if testMode {
160
160
+
fmt.Println("TEST_MODE enabled - will use HTTP for local DID resolution and transition:generic scope")
161
161
+
}
162
162
+
157
163
// Create OAuth app (indigo client)
158
158
-
oauthApp, err := oauth.NewApp(baseURL, oauthStore, defaultHoldDID)
164
164
+
oauthApp, err := oauth.NewApp(baseURL, oauthStore, defaultHoldDID, testMode)
159
165
if err != nil {
160
166
return fmt.Errorf("failed to create OAuth app: %w", err)
161
167
}
162
162
-
fmt.Println("Using full OAuth scopes (including blob: scope)")
168
168
+
if testMode {
169
169
+
fmt.Println("Using OAuth scopes with transition:generic (test mode)")
170
170
+
} else {
171
171
+
fmt.Println("Using OAuth scopes with RPC scope (production mode)")
172
172
+
}
163
173
164
174
// Invalidate sessions with mismatched scopes on startup
165
175
// This ensures all users have the latest required scopes after deployment
166
166
-
desiredScopes := oauth.GetDefaultScopes(defaultHoldDID)
176
176
+
desiredScopes := oauth.GetDefaultScopes(defaultHoldDID, testMode)
167
177
invalidatedCount, err := oauthStore.InvalidateSessionsWithMismatchedScopes(context.Background(), desiredScopes)
168
178
if err != nil {
169
179
fmt.Printf("Warning: Failed to invalidate sessions with mismatched scopes: %v\n", err)
···
185
195
// Set global database for pull/push metrics tracking
186
196
metricsDB := db.NewMetricsDB(uiDatabase)
187
197
middleware.SetGlobalDatabase(metricsDB)
188
188
-
189
189
-
// Extract test mode from config
190
190
-
testMode := appview.ExtractTestMode(config)
191
191
-
if testMode {
192
192
-
fmt.Println("TEST_MODE enabled - will use HTTP for local DID resolution")
193
193
-
}
194
198
195
199
// Create RemoteHoldAuthorizer for hold authorization with caching
196
200
holdAuthorizer := auth.NewRemoteHoldAuthorizer(uiDatabase, testMode)
+104
docs/appview.md
···
1
1
+
# ATCR AppView
2
2
+
3
3
+
The **AppView** is the OCI-compliant registry server for ATCR (ATProto Container Registry). It provides the Docker Registry HTTP API V2 and a web interface for browsing container images.
4
4
+
5
5
+
## What is AppView?
6
6
+
7
7
+
AppView serves as the central registry server that:
8
8
+
9
9
+
- **Serves OCI Distribution API** - Compatible with Docker, containerd, podman, and other OCI clients
10
10
+
- **Resolves ATProto identities** - Converts handles and DIDs to PDS endpoints
11
11
+
- **Routes manifests** - Stores container manifests as ATProto records in users' Personal Data Servers
12
12
+
- **Routes blobs** - Proxies blob operations to hold services (S3-compatible storage)
13
13
+
- **Provides web UI** - Browse, search, and star repositories
14
14
+
15
15
+
## Image Format
16
16
+
17
17
+
Container images use ATProto identities:
18
18
+
19
19
+
```
20
20
+
atcr.io/alice.bsky.social/myapp:latest
21
21
+
atcr.io/did:plc:xyz123/myapp:latest
22
22
+
```
23
23
+
24
24
+
## Using ATCR
25
25
+
26
26
+
### Push Images
27
27
+
28
28
+
```bash
29
29
+
# Install credential helper
30
30
+
curl -fsSL https://atcr.io/install.sh | bash
31
31
+
32
32
+
# Configure Docker (add to ~/.docker/config.json)
33
33
+
{
34
34
+
"credHelpers": {
35
35
+
"atcr.io": "atcr"
36
36
+
}
37
37
+
}
38
38
+
39
39
+
# Push images (authenticates automatically)
40
40
+
docker tag myapp:latest atcr.io/yourhandle/myapp:latest
41
41
+
docker push atcr.io/yourhandle/myapp:latest
42
42
+
```
43
43
+
44
44
+
### Pull Images
45
45
+
46
46
+
```bash
47
47
+
# Public images (no auth required)
48
48
+
docker pull atcr.io/alice.bsky.social/myapp:latest
49
49
+
50
50
+
# Private images (automatic OAuth authentication)
51
51
+
docker pull atcr.io/yourhandle/private-app:latest
52
52
+
```
53
53
+
54
54
+
## Running Your Own AppView
55
55
+
56
56
+
Deploy your own registry instance with Docker Compose:
57
57
+
58
58
+
```bash
59
59
+
# Create configuration
60
60
+
cp .env.appview.example .env.appview
61
61
+
# Edit .env.appview with your settings
62
62
+
63
63
+
# Start services
64
64
+
docker-compose up -d
65
65
+
```
66
66
+
67
67
+
### Configuration
68
68
+
69
69
+
Key environment variables:
70
70
+
71
71
+
- `ATCR_HTTP_ADDR` - HTTP listen address (default: `:5000`)
72
72
+
- `ATCR_BASE_URL` - Public URL for OAuth/JWT realm
73
73
+
- `ATCR_DEFAULT_HOLD_DID` - Default hold service DID for blob storage (required)
74
74
+
- `ATCR_UI_ENABLED` - Enable web interface (default: `true`)
75
75
+
- `JETSTREAM_URL` - ATProto event stream URL for real-time updates
76
76
+
77
77
+
See [deployment documentation](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/deploy/README.md) for production setup.
78
78
+
79
79
+
## Features
80
80
+
81
81
+
- ✅ **OCI-compliant** - Full Docker Registry API V2 support
82
82
+
- ✅ **ATProto OAuth** - Secure authentication with DPoP
83
83
+
- ✅ **Decentralized storage** - Manifests stored in users' PDS
84
84
+
- ✅ **Web UI** - Browse repositories, view tags, search images
85
85
+
- ✅ **Real-time updates** - Jetstream integration for live indexing
86
86
+
- ✅ **Multi-arch support** - ARM64, AMD64, and other platforms
87
87
+
- ✅ **BYOS** - Bring Your Own Storage via hold services
88
88
+
89
89
+
## Storage Architecture
90
90
+
91
91
+
**Hybrid model:**
92
92
+
- **Manifests** → ATProto records in user's PDS (small JSON metadata)
93
93
+
- **Blobs** → Hold services with S3-compatible backends (large binary layers)
94
94
+
95
95
+
This design keeps metadata portable and federated while leveraging cheap blob storage for layers.
96
96
+
97
97
+
## License
98
98
+
99
99
+
MIT
100
100
+
101
101
+
---
102
102
+
103
103
+
**Documentation:** https://tangled.org/@evan.jarrett.net/at-container-registry
104
104
+
**Source Code:** https://tangled.org/@evan.jarrett.net/at-container-registry
+2
-2
go.mod
···
20
20
github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4
21
21
github.com/klauspost/compress v1.18.0
22
22
github.com/mattn/go-sqlite3 v1.14.32
23
23
+
github.com/microcosm-cc/bluemonday v1.0.27
23
24
github.com/multiformats/go-multihash v0.2.3
24
25
github.com/opencontainers/go-digest v1.0.0
25
26
github.com/spf13/cobra v1.8.0
26
27
github.com/whyrusleeping/cbor-gen v0.3.1
28
28
+
github.com/yuin/goldmark v1.7.13
27
29
go.opentelemetry.io/otel v1.32.0
28
30
go.yaml.in/yaml/v4 v4.0.0-rc.2
29
31
golang.org/x/crypto v0.39.0
···
87
89
github.com/jmespath/go-jmespath v0.4.0 // indirect
88
90
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
89
91
github.com/mattn/go-isatty v0.0.20 // indirect
90
90
-
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
91
92
github.com/minio/sha256-simd v1.0.1 // indirect
92
93
github.com/mr-tron/base58 v1.2.0 // indirect
93
94
github.com/multiformats/go-base32 v0.1.0 // indirect
···
108
109
github.com/sirupsen/logrus v1.9.3 // indirect
109
110
github.com/spaolacci/murmur3 v1.1.0 // indirect
110
111
github.com/spf13/pflag v1.0.5 // indirect
111
111
-
github.com/yuin/goldmark v1.7.13 // indirect
112
112
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
113
113
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
114
114
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 // indirect
+1
-1
pkg/appview/readme/fetcher.go
···
29
29
// Configure markdown renderer with GitHub-flavored markdown
30
30
md := goldmark.New(
31
31
goldmark.WithExtensions(
32
32
-
extension.GFM, // GitHub Flavored Markdown
32
32
+
extension.GFM, // GitHub Flavored Markdown
33
33
extension.Typographer, // Smart quotes, dashes, etc.
34
34
),
35
35
goldmark.WithParserOptions(
+20
-7
pkg/auth/oauth/client.go
···
20
20
}
21
21
22
22
// NewApp creates a new OAuth app for ATCR with default scopes
23
23
-
func NewApp(baseURL string, store oauth.ClientAuthStore, holdDid string) (*App, error) {
24
24
-
return NewAppWithScopes(baseURL, store, GetDefaultScopes(holdDid))
23
23
+
func NewApp(baseURL string, store oauth.ClientAuthStore, holdDid string, testMode bool) (*App, error) {
24
24
+
return NewAppWithScopes(baseURL, store, GetDefaultScopes(holdDid, testMode))
25
25
}
26
26
27
27
// NewAppWithScopes creates a new OAuth app for ATCR with custom scopes
···
120
120
}
121
121
122
122
// GetDefaultScopes returns the default OAuth scopes for ATCR registry operations
123
123
-
func GetDefaultScopes(did string) []string {
124
124
-
return []string{
123
123
+
// testMode determines whether to use transition:generic (test) or rpc scopes (production)
124
124
+
func GetDefaultScopes(did string, testMode bool) []string {
125
125
+
scopes := []string{
125
126
"atproto",
126
126
-
"transition:generic",
127
127
// Image manifest types (single-arch)
128
128
"blob:application/vnd.oci.image.manifest.v1+json",
129
129
"blob:application/vnd.docker.distribution.manifest.v2+json",
···
132
132
"blob:application/vnd.docker.distribution.manifest.list.v2+json",
133
133
// OCI artifact manifests (for cosign signatures, SBOMs, attestations)
134
134
"blob:application/vnd.cncf.oras.artifact.manifest.v1+json",
135
135
-
fmt.Sprintf("rpc:com.atproto.repo.getRecord?aud=%s#atcr_hold", did),
135
135
+
}
136
136
+
137
137
+
// In test mode: use transition:generic (local dev with test PDS)
138
138
+
// In production: use rpc scope for service auth
139
139
+
if testMode {
140
140
+
scopes = append(scopes, "transition:generic")
141
141
+
} else {
142
142
+
scopes = append(scopes, fmt.Sprintf("rpc:com.atproto.repo.getRecord?aud=%s#atcr_hold", did))
143
143
+
}
144
144
+
145
145
+
// Add repo scopes
146
146
+
scopes = append(scopes,
136
147
fmt.Sprintf("repo:%s", atproto.ManifestCollection),
137
148
fmt.Sprintf("repo:%s", atproto.TagCollection),
138
149
fmt.Sprintf("repo:%s", atproto.StarCollection),
139
150
fmt.Sprintf("repo:%s", atproto.SailorProfileCollection),
140
140
-
}
151
151
+
)
152
152
+
153
153
+
return scopes
141
154
}
142
155
143
156
// ScopesMatch checks if two scope lists are equivalent (order-independent)
+3
-1
pkg/auth/oauth/interactive.go
···
33
33
}
34
34
35
35
// Create OAuth app with custom scopes (or defaults if nil)
36
36
+
// Interactive flows are typically for production use (credential helper, etc.)
37
37
+
// so we default to testMode=false
36
38
var app *App
37
39
if scopes != nil {
38
40
app, err = NewAppWithScopes(baseURL, store, scopes)
39
41
} else {
40
40
-
app, err = NewApp(baseURL, store, "*")
42
42
+
app, err = NewApp(baseURL, store, "*", false)
41
43
}
42
44
if err != nil {
43
45
return nil, fmt.Errorf("failed to create OAuth app: %w", err)
test-e2e.sh
scripts/test-e2e.sh