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.

implement search, some code cleanup

+199 -461
-30
.claude/settings.json
··· 1 - { 2 - "permissions": { 3 - "allow": [ 4 - "WebSearch", 5 - "WebFetch(domain:github.com)", 6 - "WebFetch(domain:pkg.go.dev)", 7 - "WebFetch(domain:distribution.github.io)", 8 - "Write(*)", 9 - "Edit(*)", 10 - "Bash(find:*)", 11 - "Bash(curl:*)", 12 - "Bash(sed:*)", 13 - "Bash(grep:*)", 14 - "Bash(gofmt:*)", 15 - "Bash(mkdir:*)", 16 - "Bash(golangci-lint run:*)", 17 - "Bash(go run:*)", 18 - "Bash(go install:*)", 19 - "Bash(go test:*)", 20 - "Bash(go build:*)", 21 - "Bash(go tool:*)", 22 - "Bash(go vet:*)", 23 - "Bash(go get:*)", 24 - "Bash(go mod:*)", 25 - "Bash(go get:*)" 26 - ], 27 - "deny": [], 28 - "ask": [] 29 - } 30 - }
+1
.gitignore
··· 11 11 .env 12 12 13 13 # IDE 14 + .claude/ 14 15 .vscode/ 15 16 .idea/ 16 17 *.swp
+52
CLAUDE.md
··· 285 285 - Implements full `distribution.BlobStore` interface 286 286 - Used when user has `io.atcr.hold` record 287 287 288 + #### AppView Web UI (`pkg/appview/`) 289 + 290 + The AppView includes a web interface for browsing the registry: 291 + 292 + **Features:** 293 + - Repository browsing and search 294 + - Star/favorite repositories 295 + - Pull count tracking 296 + - User profiles and settings 297 + - OAuth-based authentication for web users 298 + 299 + **Database Layer** (`pkg/appview/db/`): 300 + - SQLite database for metadata (stars, pulls, repository info) 301 + - Schema migrations via SQL files in `pkg/appview/db/schema.go` 302 + - Stores: OAuth sessions, device flows, repository metadata 303 + - **NOTE:** Simple SQLite for MVP. For production multi-instance: use PostgreSQL 304 + 305 + **Jetstream Integration** (`pkg/appview/jetstream/`): 306 + - Consumes ATProto Jetstream for real-time updates 307 + - Backfills repository records from PDS 308 + - Indexes manifests, tags, and repository metadata 309 + - Worker processes incoming events 310 + 311 + **Web Handlers** (`pkg/appview/handlers/`): 312 + - `home.go` - Landing page 313 + - `repository.go` - Repository detail pages 314 + - `search.go` - Search functionality 315 + - `auth.go` - OAuth login/logout for web 316 + - `settings.go` - User settings management 317 + - `api.go` - JSON API endpoints 318 + 319 + **Static Assets** (`pkg/appview/static/`, `pkg/appview/templates/`): 320 + - Templates use Go html/template 321 + - JavaScript in `static/js/app.js` 322 + - Minimal CSS for clean UI 323 + 288 324 #### Hold Service (`cmd/hold/`) 289 325 290 326 Lightweight standalone service for BYOS (Bring Your Own Storage): ··· 403 439 - Name resolver under `middleware.registry` 404 440 - Default storage endpoint: `middleware.registry.options.default_storage_endpoint` 405 441 - Auth token signing keys and expiration 442 + - Database path: `db.path` (SQLite database location) 443 + - Jetstream endpoint: `jetstream.endpoint` (for ATProto event streaming) 406 444 407 445 **Hold Service configuration** (environment variables): 408 446 - Storage driver config via env vars: `STORAGE_DRIVER`, `AWS_*`, `S3_*` ··· 480 518 3. For custom drivers: implement `storagedriver.StorageDriver` interface 481 519 4. Add case to `buildStorageConfig()` in `cmd/hold/main.go` 482 520 5. Update `.env.example` with new driver's env vars 521 + 522 + **Working with the database**: 523 + - Schema defined in `pkg/appview/db/schema.go` 524 + - Queries in `pkg/appview/db/queries.go` 525 + - Stores for OAuth, devices, sessions in separate files 526 + - Run migrations automatically on startup 527 + - Database path configurable via config.yml 528 + 529 + **Adding web UI features**: 530 + - Add handler in `pkg/appview/handlers/` 531 + - Register route in `cmd/appview/serve.go` 532 + - Create template in `pkg/appview/templates/pages/` 533 + - Use existing auth middleware for protected routes 534 + - API endpoints return JSON, pages return HTML 483 535 484 536 ## Important Context Values 485 537
+104 -29
README.md
··· 8 8 9 9 ### Architecture 10 10 11 - - **Manifests**: Stored as ATProto records in user PDSs (small JSON metadata) 12 - - **Blobs/Layers**: Stored in S3 (large binary data) 13 - - **Name Resolution**: Supports both ATProto handles and DIDs 11 + ATCR consists of three main components: 12 + 13 + 1. **AppView** - OCI registry server + web UI 14 + - Serves OCI Distribution API (Docker push/pull) 15 + - Resolves identities (handle/DID → PDS endpoint) 16 + - Routes manifests to user's PDS, blobs to storage 17 + - Web interface for browsing and search 18 + - SQLite database for stars, pulls, metadata 19 + 20 + 2. **Hold Service** - Optional storage service (BYOS) 21 + - Lightweight HTTP server for presigned URLs 22 + - Supports S3, Storj, Minio, filesystem, etc. 23 + - Authorization via ATProto records 24 + - Users can deploy their own hold 25 + 26 + 3. **Credential Helper** - Client-side OAuth 27 + - ATProto OAuth with DPoP 28 + - Exchanges OAuth token for registry JWT 29 + - Seamless Docker integration 30 + 31 + **Storage Model:** 32 + - **Manifests** → ATProto records in user PDSs (small JSON metadata) 33 + - **Blobs/Layers** → S3 or user's hold service (large binary data) 34 + - **Name Resolution** → Supports both handles and DIDs 14 35 - `atcr.io/alice.bsky.social/myimage:latest` 15 36 - `atcr.io/did:plc:xyz123/myimage:latest` 16 37 17 38 ## Features 18 39 19 - - OCI Distribution Spec compliant 20 - - ATProto-native manifest storage 21 - - S3 blob storage for container layers 22 - - DID/handle resolution 23 - - Decentralized manifest ownership 40 + ### Core Registry 41 + - **OCI Distribution Spec compliant** - Works with Docker, containerd, podman 42 + - **ATProto-native manifest storage** - Manifests stored as records in user PDSs 43 + - **Hybrid storage** - Small manifests in ATProto, large blobs in S3/BYOS 44 + - **DID/handle resolution** - Supports both handles and DIDs for image names 45 + - **Decentralized ownership** - Users own their manifest data via their PDS 46 + 47 + ### Web Interface 48 + - **Repository browser** - Browse and search container images 49 + - **Star repositories** - Favorite images for quick access 50 + - **Pull tracking** - View popularity and usage metrics 51 + - **OAuth authentication** - Sign in with your ATProto identity 52 + - **User profiles** - Manage your default storage hold 53 + 54 + ### Authentication 55 + - **ATProto OAuth with DPoP** - Cryptographic proof-of-possession tokens 56 + - **Docker credential helper** - Seamless `docker push/pull` workflow 57 + - **Token exchange** - OAuth tokens converted to registry JWTs 58 + 59 + ### Storage 60 + - **BYOS (Bring Your Own Storage)** - Deploy your own hold service 61 + - **Multi-backend support** - S3, Storj, Minio, Azure, GCS, filesystem 62 + - **Presigned URLs** - Direct client-to-storage uploads/downloads 63 + - **Hold discovery** - Automatic routing based on user preferences 24 64 25 65 ## Building 26 66 ··· 35 75 docker build -f Dockerfile.hold -t atcr.io/hold:latest . 36 76 ``` 37 77 38 - ## Quick Start (Local Testing) 39 - 40 - **Automated setup:** 41 - ```bash 42 - # Run the test script (handles everything) 43 - ./test-local.sh 44 - ``` 45 - 46 - The script will: 47 - 1. Create necessary directories (`/var/lib/atcr/*`) 48 - 2. Build all binaries 49 - 3. Start registry and hold service 50 - 4. Show you how to test 51 - 52 78 **Manual setup:** 53 79 ```bash 54 80 # 1. Create directories ··· 192 218 193 219 ## Usage 194 220 221 + ### Configure Credential Helper (Recommended) 222 + 223 + ```bash 224 + # Build and configure the credential helper 225 + go build -o docker-credential-atcr ./cmd/credential-helper 226 + ./docker-credential-atcr configure 227 + # Follow the OAuth flow in your browser 228 + 229 + # Add to Docker config (~/.docker/config.json) 230 + { 231 + "credHelpers": { 232 + "atcr.io": "atcr" 233 + } 234 + } 235 + ``` 236 + 195 237 ### Pushing an Image 196 238 197 239 ```bash 198 240 # Tag your image 199 241 docker tag myapp:latest atcr.io/alice/myapp:latest 200 242 201 - # Push to ATCR 243 + # Push to ATCR (credential helper handles auth) 202 244 docker push atcr.io/alice/myapp:latest 203 245 ``` 204 246 ··· 209 251 docker pull atcr.io/alice/myapp:latest 210 252 ``` 211 253 254 + ### Web Interface 255 + 256 + Visit the AppView URL (default: http://localhost:5000) to: 257 + - Browse repositories 258 + - Search for images 259 + - Star your favorites 260 + - View pull statistics 261 + - Manage your storage settings 262 + 212 263 ## Development 213 264 214 265 ### Project Structure 215 266 216 267 ``` 217 268 atcr.io/ 218 - ├── cmd/appview/ # AppView entrypoint 269 + ├── cmd/ 270 + │ ├── appview/ # AppView entrypoint (registry + web UI) 271 + │ ├── hold/ # Hold service entrypoint (BYOS) 272 + │ └── credential-helper/ # Docker credential helper 219 273 ├── pkg/ 220 - │ ├── atproto/ # ATProto client and manifest store 221 - │ ├── storage/ # S3 blob store and routing 222 - │ ├── middleware/ # Registry and repository middleware 223 - │ └── server/ # HTTP handlers 224 - ├── config/ # Configuration files 274 + │ ├── appview/ # Web UI components 275 + │ │ ├── handlers/ # HTTP handlers (home, repo, search, auth) 276 + │ │ ├── db/ # SQLite database layer 277 + │ │ ├── jetstream/ # ATProto Jetstream consumer 278 + │ │ ├── static/ # JS, CSS assets 279 + │ │ └── templates/ # HTML templates 280 + │ ├── atproto/ # ATProto integration 281 + │ │ ├── client.go # PDS client 282 + │ │ ├── resolver.go # DID/handle resolution 283 + │ │ ├── manifest_store.go # OCI manifest store 284 + │ │ ├── lexicon.go # ATProto record schemas 285 + │ │ └── profile.go # Sailor profile management 286 + │ ├── storage/ # Storage layer 287 + │ │ ├── routing_repository.go # Routes manifests/blobs 288 + │ │ ├── proxy_blob_store.go # BYOS proxy 289 + │ │ ├── s3_blob_store.go # S3 wrapper 290 + │ │ └── hold_cache.go # Hold endpoint cache 291 + │ ├── middleware/ # Registry middleware 292 + │ │ ├── registry.go # Name resolution 293 + │ │ └── repository.go # Storage routing 294 + │ └── auth/ # Authentication 295 + │ ├── oauth/ # ATProto OAuth with DPoP 296 + │ ├── token/ # JWT issuer/validator 297 + │ └── atproto/ # Session validation 298 + ├── config/ # Configuration files 299 + ├── docs/ # Documentation 225 300 └── Dockerfile 226 301 ``` 227 302
SPEC.md docs/SPEC.md
+2
cmd/appview/serve.go
··· 541 541 DB: readOnlyDB, 542 542 Templates: templates, 543 543 RegistryURL: uihandlers.TrimRegistryURL(baseURL), 544 + Directory: oauthApp.Directory(), 545 + Refresher: refresher, 544 546 }, 545 547 )).Methods("GET") 546 548
+37
pkg/appview/handlers/repository.go
··· 3 3 import ( 4 4 "database/sql" 5 5 "html/template" 6 + "log" 6 7 "net/http" 7 8 8 9 "atcr.io/pkg/appview/db" 10 + "atcr.io/pkg/appview/middleware" 11 + "atcr.io/pkg/atproto" 12 + "atcr.io/pkg/auth/oauth" 13 + "github.com/bluesky-social/indigo/atproto/identity" 9 14 "github.com/gorilla/mux" 10 15 ) 11 16 ··· 14 19 DB *sql.DB 15 20 Templates *template.Template 16 21 RegistryURL string 22 + Directory identity.Directory 23 + Refresher *oauth.Refresher 17 24 } 18 25 19 26 func (h *RepositoryPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ··· 45 52 return 46 53 } 47 54 55 + // Fetch star count 56 + stats, err := db.GetRepositoryStats(h.DB, owner.DID, repository) 57 + if err != nil { 58 + log.Printf("Failed to fetch repository stats: %v", err) 59 + // Continue with zero stats on error 60 + stats = &db.RepositoryStats{StarCount: 0} 61 + } 62 + 63 + // Check if current user has starred this repo 64 + isStarred := false 65 + user := middleware.GetUser(r) 66 + if user != nil && h.Refresher != nil && h.Directory != nil { 67 + // Get OAuth session for the authenticated user 68 + session, err := h.Refresher.GetSession(r.Context(), user.DID) 69 + if err == nil { 70 + // Get user's PDS client 71 + apiClient := session.APIClient() 72 + pdsClient := atproto.NewClientWithIndigoClient(user.PDSEndpoint, user.DID, apiClient) 73 + 74 + // Check if star record exists 75 + rkey := atproto.StarRecordKey(owner.DID, repository) 76 + _, err = pdsClient.GetRecord(r.Context(), atproto.StarCollection, rkey) 77 + isStarred = (err == nil) 78 + } 79 + } 80 + 48 81 data := struct { 49 82 PageData 50 83 Owner *db.User // Repository owner 51 84 Repository *db.Repository 85 + StarCount int 86 + IsStarred bool 52 87 }{ 53 88 PageData: NewPageData(r, h.RegistryURL), 54 89 Owner: owner, 55 90 Repository: repo, 91 + StarCount: stats.StarCount, 92 + IsStarred: isStarred, 56 93 } 57 94 58 95 if err := h.Templates.ExecuteTemplate(w, "repository", data); err != nil {
-3
pkg/appview/static/js/app.js
··· 115 115 dropdownMenu.setAttribute('hidden', ''); 116 116 } 117 117 } 118 - 119 - // Load star status on repository page 120 - loadStarStatus(); 121 118 }); 122 119 123 120 // Toggle star on a repository
+3 -3
pkg/appview/templates/pages/repository.html
··· 36 36 37 37 <!-- Star Button --> 38 38 <div class="repo-actions"> 39 - <button class="star-btn" id="star-btn" onclick="toggleStar('{{ .Owner.Handle }}', '{{ .Repository.Name }}')"> 40 - <span class="star-icon" id="star-icon">☆</span> 41 - <span class="star-count" id="star-count">0</span> 39 + <button class="star-btn{{ if .IsStarred }} starred{{ end }}" id="star-btn" onclick="toggleStar('{{ .Owner.Handle }}', '{{ .Repository.Name }}')"> 40 + <span class="star-icon" id="star-icon">{{ if .IsStarred }}★{{ else }}☆{{ end }}</span> 41 + <span class="star-count" id="star-count">{{ .StarCount }}</span> 42 42 </button> 43 43 </div> 44 44
-396
test-registry.sh
··· 1 - #!/bin/bash 2 - 3 - # ATCR AppView Test Script 4 - # Tests various registry operations with ATProto storage 5 - 6 - # Configuration 7 - REGISTRY="127.0.0.1:5000" 8 - HANDLE="evan.jarrett.net" 9 - IMAGE_PREFIX="${REGISTRY}/${HANDLE}" 10 - 11 - # Colors for output 12 - GREEN='\033[0;32m' 13 - BLUE='\033[0;34m' 14 - YELLOW='\033[1;33m' 15 - RED='\033[0;31m' 16 - NC='\033[0m' # No Color 17 - 18 - # Test tracking 19 - declare -a TEST_NAMES 20 - declare -a TEST_RESULTS 21 - declare -a TEST_ERRORS 22 - TEST_COUNT=0 23 - 24 - # Helper functions 25 - log_test() { 26 - echo -e "\n${BLUE}========================================${NC}" 27 - echo -e "${BLUE}TEST: $1${NC}" 28 - echo -e "${BLUE}========================================${NC}" 29 - } 30 - 31 - log_success() { 32 - echo -e "${GREEN}✓ $1${NC}" 33 - } 34 - 35 - log_info() { 36 - echo -e "${YELLOW}ℹ $1${NC}" 37 - } 38 - 39 - log_error() { 40 - echo -e "${RED}✗ $1${NC}" 41 - } 42 - 43 - # Run a test and track results 44 - run_test() { 45 - local test_name="$1" 46 - local test_func="$2" 47 - 48 - TEST_NAMES[$TEST_COUNT]="$test_name" 49 - 50 - # Capture output and errors 51 - local output 52 - local exit_code 53 - 54 - if output=$($test_func 2>&1); then 55 - TEST_RESULTS[$TEST_COUNT]="PASS" 56 - TEST_ERRORS[$TEST_COUNT]="" 57 - echo "$output" 58 - else 59 - exit_code=$? 60 - TEST_RESULTS[$TEST_COUNT]="FAIL" 61 - TEST_ERRORS[$TEST_COUNT]="$output" 62 - echo "$output" 63 - log_error "Test failed with exit code: $exit_code" 64 - fi 65 - 66 - ((TEST_COUNT++)) 67 - } 68 - 69 - # Display test summary 70 - show_summary() { 71 - local pass_count=0 72 - local fail_count=0 73 - 74 - echo -e "\n${BLUE}╔═══════════════════════════════════════╗${NC}" 75 - echo -e "${BLUE}║ TEST SUMMARY ║${NC}" 76 - echo -e "${BLUE}╔═══════════════════════════════════════╗${NC}\n" 77 - 78 - for ((i=0; i<TEST_COUNT; i++)); do 79 - if [ "${TEST_RESULTS[$i]}" = "PASS" ]; then 80 - echo -e "${GREEN}✓ ${TEST_NAMES[$i]}${NC}" 81 - ((pass_count++)) 82 - else 83 - echo -e "${RED}✗ ${TEST_NAMES[$i]}${NC}" 84 - if [ -n "${TEST_ERRORS[$i]}" ]; then 85 - echo -e "${RED} Error: ${TEST_ERRORS[$i]:0:100}...${NC}" 86 - fi 87 - ((fail_count++)) 88 - fi 89 - done 90 - 91 - echo -e "\n${BLUE}═══════════════════════════════════════${NC}" 92 - echo -e "Total: $TEST_COUNT | ${GREEN}Passed: $pass_count${NC} | ${RED}Failed: $fail_count${NC}" 93 - echo -e "${BLUE}═══════════════════════════════════════${NC}\n" 94 - 95 - if [ $fail_count -gt 0 ]; then 96 - return 1 97 - fi 98 - return 0 99 - } 100 - 101 - # Get credentials from Docker config 102 - get_credentials() { 103 - local config_file="$HOME/.docker/config.json" 104 - 105 - if [ ! -f "$config_file" ]; then 106 - log_error "Docker config not found at $config_file" 107 - log_info "Please run: docker login ${REGISTRY}" 108 - exit 1 109 - fi 110 - 111 - # Extract auth token for our registry 112 - local auth_token=$(jq -r ".auths.\"${REGISTRY}\".auth // empty" "$config_file") 113 - 114 - if [ -z "$auth_token" ]; then 115 - log_error "No credentials found for ${REGISTRY}" 116 - log_info "Please run: docker login ${REGISTRY}" 117 - exit 1 118 - fi 119 - 120 - # Decode base64 to get username:password 121 - CREDENTIALS=$(echo "$auth_token" | base64 -d) 122 - log_success "Loaded credentials from Docker config" 123 - } 124 - 125 - # Check if logged in 126 - check_login() { 127 - log_info "Checking Docker login status..." 128 - if ! docker login --help &>/dev/null; then 129 - log_error "Docker not available" 130 - exit 1 131 - fi 132 - 133 - get_credentials 134 - } 135 - 136 - # Prepare test images 137 - prepare_images() { 138 - log_info "Preparing test images..." 139 - 140 - log_info "Pulling debian:12-slim..." 141 - docker pull debian:12-slim 142 - 143 - log_info "Tagging debian:12-slim..." 144 - docker tag debian:12-slim ${IMAGE_PREFIX}/debian:12-slim 145 - 146 - log_info "Pushing initial debian:12-slim..." 147 - docker push ${IMAGE_PREFIX}/debian:12-slim 148 - 149 - log_success "Test images prepared" 150 - } 151 - 152 - # Test 1: Multiple tags pointing to same manifest 153 - test_multiple_tags() { 154 - log_test "Multiple tags pointing to same manifest" 155 - 156 - log_info "Tagging debian:12-slim with multiple tags..." 157 - docker tag ${IMAGE_PREFIX}/debian:12-slim ${IMAGE_PREFIX}/debian:latest 158 - docker tag ${IMAGE_PREFIX}/debian:12-slim ${IMAGE_PREFIX}/debian:bookworm 159 - 160 - log_info "Pushing tags..." 161 - if ! docker push ${IMAGE_PREFIX}/debian:latest; then 162 - log_error "Failed to push debian:latest" 163 - return 1 164 - fi 165 - if ! docker push ${IMAGE_PREFIX}/debian:bookworm; then 166 - log_error "Failed to push debian:bookworm" 167 - return 1 168 - fi 169 - 170 - log_success "Multiple tags pushed successfully" 171 - log_info "All three tags should point to the same manifest digest" 172 - return 0 173 - } 174 - 175 - # Test 2: Pull by digest 176 - test_pull_by_digest() { 177 - log_test "Pull by digest (immutable reference)" 178 - 179 - # Get the manifest digest from docker inspect 180 - log_info "Getting manifest digest..." 181 - DIGEST=$(docker inspect ${IMAGE_PREFIX}/debian:12-slim --format='{{index .RepoDigests 0}}' | cut -d'@' -f2) 182 - 183 - if [ -z "$DIGEST" ]; then 184 - log_error "Could not get digest" 185 - return 1 186 - fi 187 - 188 - log_info "Digest: $DIGEST" 189 - 190 - log_info "Removing local image..." 191 - docker rmi ${IMAGE_PREFIX}/debian:12-slim 2>/dev/null || log_info "Image already removed" 192 - 193 - log_info "Pulling by digest..." 194 - if ! docker pull ${IMAGE_PREFIX}/debian@${DIGEST}; then 195 - log_error "Manifest verification failed - known issue with digest storage" 196 - log_info "The registry stores manifests correctly but digest verification may differ" 197 - # Don't fail - this is a known limitation 198 - return 0 199 - fi 200 - 201 - log_success "Pull by digest successful" 202 - return 0 203 - } 204 - 205 - # Test 3: Layer deduplication 206 - test_layer_deduplication() { 207 - log_test "Layer deduplication (shared layers)" 208 - 209 - log_info "Pulling debian:12 (larger variant)..." 210 - if ! docker pull debian:12; then 211 - log_error "Failed to pull debian:12" 212 - return 1 213 - fi 214 - 215 - log_info "Tagging and pushing debian:12..." 216 - docker tag debian:12 ${IMAGE_PREFIX}/debian:12-full 217 - if ! docker push ${IMAGE_PREFIX}/debian:12-full; then 218 - log_error "Failed to push debian:12-full" 219 - return 1 220 - fi 221 - 222 - log_success "Image with shared layers pushed" 223 - log_info "Check logs - should see 'Layer already exists' or 'Mounted from'" 224 - return 0 225 - } 226 - 227 - # Test 4: Multiple repositories 228 - test_multiple_repos() { 229 - log_test "Multiple repositories" 230 - 231 - log_info "Pulling alpine:latest..." 232 - if ! docker pull alpine:latest; then 233 - log_error "Failed to pull alpine:latest" 234 - return 1 235 - fi 236 - 237 - log_info "Tagging alpine..." 238 - docker tag alpine:latest ${IMAGE_PREFIX}/alpine:latest 239 - docker tag alpine:latest ${IMAGE_PREFIX}/alpine:3 240 - 241 - log_info "Pushing alpine..." 242 - if ! docker push ${IMAGE_PREFIX}/alpine:latest; then 243 - log_error "Failed to push alpine:latest" 244 - return 1 245 - fi 246 - if ! docker push ${IMAGE_PREFIX}/alpine:3; then 247 - log_error "Failed to push alpine:3" 248 - return 1 249 - fi 250 - 251 - log_success "Multiple repositories created" 252 - return 0 253 - } 254 - 255 - # Test 5: Catalog API 256 - test_catalog_api() { 257 - log_test "Catalog API (list repositories)" 258 - 259 - log_info "Fetching repository catalog..." 260 - local response=$(curl -s -u "${CREDENTIALS}" http://${REGISTRY}/v2/_catalog) 261 - 262 - echo "$response" | jq . 263 - 264 - if echo "$response" | grep -q '"errors"'; then 265 - log_info "Expected: Registry requires OAuth tokens for API access (not basic auth)" 266 - log_success "Catalog API responded (OAuth required)" 267 - return 0 268 - fi 269 - 270 - log_success "Catalog API works" 271 - return 0 272 - } 273 - 274 - # Test 6: List tags 275 - test_list_tags() { 276 - log_test "List tags for repository" 277 - 278 - log_info "Listing tags for debian repository..." 279 - local debian_response=$(curl -s -u "${CREDENTIALS}" http://${REGISTRY}/v2/${HANDLE}/debian/tags/list) 280 - echo "$debian_response" | jq . 281 - 282 - if echo "$debian_response" | grep -q '"errors"'; then 283 - log_info "Expected: Registry requires OAuth tokens for API access (not basic auth)" 284 - log_success "Tags API responded (OAuth required)" 285 - return 0 286 - fi 287 - 288 - log_success "Tag listing works" 289 - return 0 290 - } 291 - 292 - # Test 7: Inspect manifest 293 - test_inspect_manifest() { 294 - log_test "Inspect manifest directly" 295 - 296 - log_info "Fetching manifest for debian:12-slim..." 297 - local manifest_response=$(curl -s -u "${CREDENTIALS}" \ 298 - -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ 299 - http://${REGISTRY}/v2/${HANDLE}/debian/manifests/12-slim) 300 - 301 - echo "$manifest_response" | jq . 302 - 303 - if echo "$manifest_response" | grep -q '"errors"'; then 304 - log_info "Expected: Registry requires OAuth tokens for API access (not basic auth)" 305 - log_success "Manifest API responded (OAuth required)" 306 - return 0 307 - fi 308 - 309 - log_success "Manifest inspection works" 310 - return 0 311 - } 312 - 313 - # Test 8: Re-pull after clearing cache 314 - test_repull() { 315 - log_test "Re-pull after clearing local cache" 316 - 317 - log_info "Removing all local ATCR images..." 318 - docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${REGISTRY}" | xargs -r docker rmi 2>/dev/null || log_info "No images to remove" 319 - 320 - log_info "Pulling debian:latest from ATCR..." 321 - if ! docker pull ${IMAGE_PREFIX}/debian:latest; then 322 - log_error "Failed to pull debian:latest" 323 - return 1 324 - fi 325 - 326 - log_info "Pulling alpine:latest from ATCR..." 327 - if ! docker pull ${IMAGE_PREFIX}/alpine:latest; then 328 - log_error "Failed to pull alpine:latest" 329 - return 1 330 - fi 331 - 332 - log_success "Re-pull from ATProto storage successful" 333 - 334 - log_info "Verifying images..." 335 - docker images | grep "${REGISTRY}" 336 - return 0 337 - } 338 - 339 - # Test 9: Check ATProto records in logs 340 - test_check_logs() { 341 - log_test "Check ATProto records in logs" 342 - 343 - log_info "Recent manifest PUT operations:" 344 - docker logs atcr-appview 2>&1 | grep "Manifests()" | tail -5 || log_info "No manifest logs found" 345 - 346 - log_info "Recent tag operations:" 347 - docker logs atcr-appview 2>&1 | grep "debian_12-slim\|debian_latest\|alpine_latest" | tail -10 || log_info "No tag logs found" 348 - 349 - log_info "Using cached access token:" 350 - docker logs atcr-appview 2>&1 | grep "Using cached access token" | tail -3 || log_info "No token cache logs found" 351 - 352 - log_success "Log check complete" 353 - return 0 354 - } 355 - 356 - # Test 10: HEAD request (check blob existence) 357 - test_head_request() { 358 - log_test "HEAD request (check blob existence)" 359 - 360 - log_info "Skipping: Direct API calls require OAuth tokens" 361 - log_info "Docker client handles blob access via credential helper" 362 - log_success "Blob access works via Docker (tested in previous tests)" 363 - return 0 364 - } 365 - 366 - # Main test runner 367 - main() { 368 - echo -e "${GREEN}" 369 - echo "╔═══════════════════════════════════════╗" 370 - echo "║ ATCR AppView Test Suite ║" 371 - echo "║ Testing ATProto + OCI Registry ║" 372 - echo "╚═══════════════════════════════════════╝" 373 - echo -e "${NC}" 374 - 375 - check_login 376 - prepare_images 377 - 378 - # Run tests 379 - run_test "Multiple tags pointing to same manifest" test_multiple_tags 380 - run_test "Pull by digest (immutable reference)" test_pull_by_digest 381 - run_test "Layer deduplication (shared layers)" test_layer_deduplication 382 - run_test "Multiple repositories" test_multiple_repos 383 - run_test "Catalog API (list repositories)" test_catalog_api 384 - run_test "List tags for repository" test_list_tags 385 - run_test "Inspect manifest directly" test_inspect_manifest 386 - run_test "Re-pull after clearing cache" test_repull 387 - run_test "Check ATProto records in logs" test_check_logs 388 - run_test "HEAD request (blob existence)" test_head_request 389 - 390 - # Show summary 391 - show_summary 392 - exit $? 393 - } 394 - 395 - # Run tests 396 - main "$@"