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

upcloud provision fixes and relay tweaks

evan.jarrett.net 11a8be14 fcc5fa78

verified
+91 -19
+1
deploy/upcloud/configs/cloudinit.sh.tmpl
··· 18 18 export DEBIAN_FRONTEND=noninteractive 19 19 apt-get update && apt-get upgrade -y 20 20 apt-get install -y git gcc make curl libsqlite3-dev nodejs npm htop systemd-timesyncd 21 + sed -i 's/^#NTP=.*/NTP=0.debian.pool.ntp.org 1.debian.pool.ntp.org 2.debian.pool.ntp.org 3.debian.pool.ntp.org/' /etc/systemd/timesyncd.conf 21 22 timedatectl set-ntp true 22 23 23 24 # Swap (for small instances)
+11 -1
deploy/upcloud/provision.go
··· 647 647 Comment: "Allow private network", 648 648 }, 649 649 { 650 + Direction: upcloud.FirewallRuleDirectionIn, 651 + Action: upcloud.FirewallRuleActionAccept, 652 + Family: upcloud.IPAddressFamilyIPv4, 653 + Protocol: upcloud.FirewallRuleProtocolUDP, 654 + SourcePortStart: "123", 655 + SourcePortEnd: "123", 656 + Position: 3, 657 + Comment: "Allow NTP replies", 658 + }, 659 + { 650 660 Direction: upcloud.FirewallRuleDirectionIn, 651 661 Action: upcloud.FirewallRuleActionDrop, 652 - Position: 3, 662 + Position: 4, 653 663 Comment: "Drop all other inbound", 654 664 }, 655 665 },
+22 -17
pkg/atproto/relays.go
··· 67 67 Online bool 68 68 Error string 69 69 HasRequestCrawl bool 70 + RequestCrawlStatus int // HTTP status code from probe (400=open, 401/403=auth required, 5xx=error) 70 71 HasListReposByCollection bool 71 72 RepoStatus *RepoStatus 72 73 HostStatus *HostStatus ··· 90 91 // Probe requestCrawl 91 92 go func() { 92 93 defer wg.Done() 93 - supported, online := probeRequestCrawl(relayURL) 94 + supported, statusCode, online := probeRequestCrawl(relayURL) 94 95 if online { 95 96 markOnline() 96 97 } 97 - if supported { 98 - mu.Lock() 99 - result.HasRequestCrawl = true 100 - mu.Unlock() 101 - } 98 + mu.Lock() 99 + result.HasRequestCrawl = supported 100 + result.RequestCrawlStatus = statusCode 101 + mu.Unlock() 102 102 }() 103 103 104 104 // Check host status ··· 158 158 return result 159 159 } 160 160 161 - // probeRequestCrawl checks if a relay supports the requestCrawl endpoint using a HEAD request. 162 - // A 4xx response (e.g. 405 Method Not Allowed) means the endpoint exists. 163 - // A 5xx or connection failure means it's broken or unsupported. 164 - func probeRequestCrawl(relayURL string) (supported bool, online bool) { 161 + // probeRequestCrawl checks if a relay supports the requestCrawl endpoint by POSTing 162 + // an empty hostname. Returns (supported, statusCode, online): 163 + // - 400 = endpoint exists and accepts unauthenticated crawls (supported=true) 164 + // - 401/403 = endpoint exists but requires auth (supported=false) 165 + // - 5xx = endpoint is broken (supported=false) 166 + // - connection error = relay offline (online=false) 167 + func probeRequestCrawl(relayURL string) (supported bool, statusCode int, online bool) { 165 168 client := &http.Client{Timeout: 5 * time.Second} 166 - req, err := http.NewRequest("HEAD", relayURL+SyncRequestCrawl, nil) 169 + body := bytes.NewReader([]byte(`{"hostname":""}`)) 170 + req, err := http.NewRequest("POST", relayURL+SyncRequestCrawl, body) 167 171 if err != nil { 168 - return false, false 172 + return false, 0, false 169 173 } 174 + req.Header.Set("Content-Type", "application/json") 170 175 171 176 resp, err := client.Do(req) 172 177 if err != nil { 173 - return false, false 178 + return false, 0, false 174 179 } 175 180 defer resp.Body.Close() 176 181 177 - // Any HTTP response means the relay is online. 178 - // 4xx (typically 405 Method Not Allowed) = endpoint exists. 179 - // 5xx = endpoint is broken. 180 - return resp.StatusCode >= 400 && resp.StatusCode < 500, true 182 + // 400 = endpoint exists, accepts unauthenticated requests (empty hostname rejected as expected) 183 + // 401/403 = endpoint exists but requires authentication 184 + // 5xx = endpoint is broken 185 + return resp.StatusCode == http.StatusBadRequest, resp.StatusCode, true 181 186 } 182 187 183 188 // probeListReposByCollection checks if a relay supports the listReposByCollection endpoint.
+48
pkg/atproto/relays_test.go
··· 217 217 func TestCheckRelayStatus_AllEndpointsSucceed(t *testing.T) { 218 218 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 219 219 switch r.URL.Path { 220 + case SyncRequestCrawl: 221 + w.WriteHeader(http.StatusBadRequest) // empty hostname = 400 220 222 case SyncGetHostStatus: 221 223 json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true}) 222 224 case SyncGetRepoStatus: ··· 237 239 if status.Error != "" { 238 240 t.Errorf("expected no error, got %q", status.Error) 239 241 } 242 + if !status.HasRequestCrawl { 243 + t.Error("expected HasRequestCrawl = true") 244 + } 245 + if status.RequestCrawlStatus != http.StatusBadRequest { 246 + t.Errorf("RequestCrawlStatus = %d, want %d", status.RequestCrawlStatus, http.StatusBadRequest) 247 + } 240 248 if !status.HasListReposByCollection { 241 249 t.Error("expected HasListReposByCollection = true") 242 250 } ··· 257 265 func TestCheckRelayStatus_OnlineButUnknownHost(t *testing.T) { 258 266 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 259 267 switch r.URL.Path { 268 + case SyncRequestCrawl: 269 + w.WriteHeader(http.StatusBadRequest) // empty hostname = 400 260 270 case SyncGetHostStatus: 261 271 w.WriteHeader(http.StatusBadRequest) // relay doesn't know this host 262 272 case SyncGetRepoStatus: ··· 303 313 func TestCheckRelayStatus_NoListReposByCollection(t *testing.T) { 304 314 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 305 315 switch r.URL.Path { 316 + case SyncRequestCrawl: 317 + w.WriteHeader(http.StatusBadRequest) // empty hostname = 400 306 318 case SyncGetHostStatus: 307 319 json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true}) 308 320 case SyncGetRepoStatus: ··· 327 339 t.Error("expected RepoStatus to be active") 328 340 } 329 341 } 342 + 343 + func TestCheckRelayStatus_AuthRequired(t *testing.T) { 344 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 345 + switch r.URL.Path { 346 + case SyncRequestCrawl: 347 + w.WriteHeader(http.StatusForbidden) // auth required 348 + case SyncGetHostStatus: 349 + json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true}) 350 + case SyncGetRepoStatus: 351 + json.NewEncoder(w).Encode(RepoStatus{DID: "did:web:hold.example.com", Active: true, Rev: "r1"}) 352 + case SyncListReposByCollection: 353 + json.NewEncoder(w).Encode(map[string]any{"repos": []any{}}) 354 + default: 355 + w.WriteHeader(http.StatusNotFound) 356 + } 357 + })) 358 + defer srv.Close() 359 + 360 + status := CheckRelayStatus(srv.URL, "hold.example.com", "did:web:hold.example.com") 361 + 362 + if !status.Online { 363 + t.Error("expected Online = true") 364 + } 365 + if status.HasRequestCrawl { 366 + t.Error("expected HasRequestCrawl = false (auth required)") 367 + } 368 + if status.RequestCrawlStatus != http.StatusForbidden { 369 + t.Errorf("RequestCrawlStatus = %d, want %d", status.RequestCrawlStatus, http.StatusForbidden) 370 + } 371 + if !status.HasListReposByCollection { 372 + t.Error("expected HasListReposByCollection = true") 373 + } 374 + if status.RepoStatus == nil || !status.RepoStatus.Active { 375 + t.Error("expected RepoStatus to be active") 376 + } 377 + }
+2
pkg/hold/admin/handlers_relays.go
··· 22 22 Online bool 23 23 Error string 24 24 HasRequestCrawl bool 25 + RequestCrawlStatus int 25 26 HasListReposByCollection bool 26 27 RepoStatus *atproto.RepoStatus 27 28 HostStatus *atproto.HostStatus ··· 77 78 Online: status.Online, 78 79 Error: status.Error, 79 80 HasRequestCrawl: status.HasRequestCrawl, 81 + RequestCrawlStatus: status.RequestCrawlStatus, 80 82 HasListReposByCollection: status.HasListReposByCollection, 81 83 RepoStatus: status.RepoStatus, 82 84 HostStatus: status.HostStatus,
+7 -1
pkg/hold/admin/templates/partials/relay_status.html
··· 22 22 <td> 23 23 <div class="flex flex-wrap gap-1"> 24 24 {{if .Online}} 25 - {{if .HasRequestCrawl}}<span class="badge badge-ghost badge-sm">requestCrawl</span>{{end}} 25 + {{if .HasRequestCrawl}} 26 + <span class="badge badge-ghost badge-sm">requestCrawl</span> 27 + {{else if or (eq .RequestCrawlStatus 401) (eq .RequestCrawlStatus 403)}} 28 + <span class="badge badge-warning badge-sm">requestCrawl (auth required)</span> 29 + {{else if ge .RequestCrawlStatus 500}} 30 + <span class="badge badge-error badge-sm">requestCrawl ({{.RequestCrawlStatus}})</span> 31 + {{end}} 26 32 {{if .HasListReposByCollection}}<span class="badge badge-ghost badge-sm">listReposByCollection</span>{{end}} 27 33 {{else}} 28 34 <span class="text-base-content/30 text-sm">-</span>