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
upcloud provision fixes and relay tweaks
evan.jarrett.net
1 week ago
11a8be14
fcc5fa78
verified
This commit was signed with the committer's
known signature
.
evan.jarrett.net
SSH Key Fingerprint:
SHA256:bznk0uVPp7XFOl67P0uTM1pCjf2A4ojeP/lsUE7uauQ=
+91
-19
6 changed files
expand all
collapse all
unified
split
deploy
upcloud
configs
cloudinit.sh.tmpl
provision.go
pkg
atproto
relays.go
relays_test.go
hold
admin
handlers_relays.go
templates
partials
relay_status.html
+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
0
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
{
0
0
0
0
0
0
0
0
0
0
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
0
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) {
0
0
0
165
client := &http.Client{Timeout: 5 * time.Second}
166
-
req, err := http.NewRequest("HEAD", relayURL+SyncRequestCrawl, nil)
0
167
if err != nil {
168
-
return false, false
169
}
0
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()
0
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 {
0
0
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
}
0
0
0
0
0
0
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 {
0
0
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 {
0
0
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
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
0
25
HasListReposByCollection bool
26
RepoStatus *atproto.RepoStatus
27
HostStatus *atproto.HostStatus
···
77
Online: status.Online,
78
Error: status.Error,
79
HasRequestCrawl: status.HasRequestCrawl,
0
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}}
0
0
0
0
0
0
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>