A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
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 "$@"