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 export DEBIAN_FRONTEND=noninteractive 19 apt-get update && apt-get upgrade -y 20 apt-get install -y git gcc make curl libsqlite3-dev nodejs npm htop systemd-timesyncd 21 timedatectl set-ntp true 22 23 # Swap (for small instances)
··· 18 export DEBIAN_FRONTEND=noninteractive 19 apt-get update && apt-get upgrade -y 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 22 timedatectl set-ntp true 23 24 # Swap (for small instances)
+11 -1
deploy/upcloud/provision.go
··· 647 Comment: "Allow private network", 648 }, 649 { 650 Direction: upcloud.FirewallRuleDirectionIn, 651 Action: upcloud.FirewallRuleActionDrop, 652 - Position: 3, 653 Comment: "Drop all other inbound", 654 }, 655 },
··· 647 Comment: "Allow private network", 648 }, 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 + { 660 Direction: upcloud.FirewallRuleDirectionIn, 661 Action: upcloud.FirewallRuleActionDrop, 662 + Position: 4, 663 Comment: "Drop all other inbound", 664 }, 665 },
+22 -17
pkg/atproto/relays.go
··· 67 Online bool 68 Error string 69 HasRequestCrawl bool 70 HasListReposByCollection bool 71 RepoStatus *RepoStatus 72 HostStatus *HostStatus ··· 90 // Probe requestCrawl 91 go func() { 92 defer wg.Done() 93 - supported, online := probeRequestCrawl(relayURL) 94 if online { 95 markOnline() 96 } 97 - if supported { 98 - mu.Lock() 99 - result.HasRequestCrawl = true 100 - mu.Unlock() 101 - } 102 }() 103 104 // Check host status ··· 158 return result 159 } 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) { 165 client := &http.Client{Timeout: 5 * time.Second} 166 - req, err := http.NewRequest("HEAD", relayURL+SyncRequestCrawl, nil) 167 if err != nil { 168 - return false, false 169 } 170 171 resp, err := client.Do(req) 172 if err != nil { 173 - return false, false 174 } 175 defer resp.Body.Close() 176 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 181 } 182 183 // probeListReposByCollection checks if a relay supports the listReposByCollection endpoint.
··· 67 Online bool 68 Error string 69 HasRequestCrawl bool 70 + RequestCrawlStatus int // HTTP status code from probe (400=open, 401/403=auth required, 5xx=error) 71 HasListReposByCollection bool 72 RepoStatus *RepoStatus 73 HostStatus *HostStatus ··· 91 // Probe requestCrawl 92 go func() { 93 defer wg.Done() 94 + supported, statusCode, online := probeRequestCrawl(relayURL) 95 if online { 96 markOnline() 97 } 98 + mu.Lock() 99 + result.HasRequestCrawl = supported 100 + result.RequestCrawlStatus = statusCode 101 + mu.Unlock() 102 }() 103 104 // Check host status ··· 158 return result 159 } 160 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) { 168 client := &http.Client{Timeout: 5 * time.Second} 169 + body := bytes.NewReader([]byte(`{"hostname":""}`)) 170 + req, err := http.NewRequest("POST", relayURL+SyncRequestCrawl, body) 171 if err != nil { 172 + return false, 0, false 173 } 174 + req.Header.Set("Content-Type", "application/json") 175 176 resp, err := client.Do(req) 177 if err != nil { 178 + return false, 0, false 179 } 180 defer resp.Body.Close() 181 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 186 } 187 188 // probeListReposByCollection checks if a relay supports the listReposByCollection endpoint.
+48
pkg/atproto/relays_test.go
··· 217 func TestCheckRelayStatus_AllEndpointsSucceed(t *testing.T) { 218 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 219 switch r.URL.Path { 220 case SyncGetHostStatus: 221 json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true}) 222 case SyncGetRepoStatus: ··· 237 if status.Error != "" { 238 t.Errorf("expected no error, got %q", status.Error) 239 } 240 if !status.HasListReposByCollection { 241 t.Error("expected HasListReposByCollection = true") 242 } ··· 257 func TestCheckRelayStatus_OnlineButUnknownHost(t *testing.T) { 258 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 259 switch r.URL.Path { 260 case SyncGetHostStatus: 261 w.WriteHeader(http.StatusBadRequest) // relay doesn't know this host 262 case SyncGetRepoStatus: ··· 303 func TestCheckRelayStatus_NoListReposByCollection(t *testing.T) { 304 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 305 switch r.URL.Path { 306 case SyncGetHostStatus: 307 json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true}) 308 case SyncGetRepoStatus: ··· 327 t.Error("expected RepoStatus to be active") 328 } 329 }
··· 217 func TestCheckRelayStatus_AllEndpointsSucceed(t *testing.T) { 218 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 219 switch r.URL.Path { 220 + case SyncRequestCrawl: 221 + w.WriteHeader(http.StatusBadRequest) // empty hostname = 400 222 case SyncGetHostStatus: 223 json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true}) 224 case SyncGetRepoStatus: ··· 239 if status.Error != "" { 240 t.Errorf("expected no error, got %q", status.Error) 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 + } 248 if !status.HasListReposByCollection { 249 t.Error("expected HasListReposByCollection = true") 250 } ··· 265 func TestCheckRelayStatus_OnlineButUnknownHost(t *testing.T) { 266 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 267 switch r.URL.Path { 268 + case SyncRequestCrawl: 269 + w.WriteHeader(http.StatusBadRequest) // empty hostname = 400 270 case SyncGetHostStatus: 271 w.WriteHeader(http.StatusBadRequest) // relay doesn't know this host 272 case SyncGetRepoStatus: ··· 313 func TestCheckRelayStatus_NoListReposByCollection(t *testing.T) { 314 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 315 switch r.URL.Path { 316 + case SyncRequestCrawl: 317 + w.WriteHeader(http.StatusBadRequest) // empty hostname = 400 318 case SyncGetHostStatus: 319 json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true}) 320 case SyncGetRepoStatus: ··· 339 t.Error("expected RepoStatus to be active") 340 } 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 Online bool 23 Error string 24 HasRequestCrawl bool 25 HasListReposByCollection bool 26 RepoStatus *atproto.RepoStatus 27 HostStatus *atproto.HostStatus ··· 77 Online: status.Online, 78 Error: status.Error, 79 HasRequestCrawl: status.HasRequestCrawl, 80 HasListReposByCollection: status.HasListReposByCollection, 81 RepoStatus: status.RepoStatus, 82 HostStatus: status.HostStatus,
··· 22 Online bool 23 Error string 24 HasRequestCrawl bool 25 + RequestCrawlStatus int 26 HasListReposByCollection bool 27 RepoStatus *atproto.RepoStatus 28 HostStatus *atproto.HostStatus ··· 78 Online: status.Online, 79 Error: status.Error, 80 HasRequestCrawl: status.HasRequestCrawl, 81 + RequestCrawlStatus: status.RequestCrawlStatus, 82 HasListReposByCollection: status.HasListReposByCollection, 83 RepoStatus: status.RepoStatus, 84 HostStatus: status.HostStatus,
+7 -1
pkg/hold/admin/templates/partials/relay_status.html
··· 22 <td> 23 <div class="flex flex-wrap gap-1"> 24 {{if .Online}} 25 - {{if .HasRequestCrawl}}<span class="badge badge-ghost badge-sm">requestCrawl</span>{{end}} 26 {{if .HasListReposByCollection}}<span class="badge badge-ghost badge-sm">listReposByCollection</span>{{end}} 27 {{else}} 28 <span class="text-base-content/30 text-sm">-</span>
··· 22 <td> 23 <div class="flex flex-wrap gap-1"> 24 {{if .Online}} 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}} 32 {{if .HasListReposByCollection}}<span class="badge badge-ghost badge-sm">listReposByCollection</span>{{end}} 33 {{else}} 34 <span class="text-base-content/30 text-sm">-</span>