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=
2/2
lint.yaml
success
4min 14s
tests.yml
success
3min 59s
+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
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
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
650
+
Direction: upcloud.FirewallRuleDirectionIn,
651
651
+
Action: upcloud.FirewallRuleActionAccept,
652
652
+
Family: upcloud.IPAddressFamilyIPv4,
653
653
+
Protocol: upcloud.FirewallRuleProtocolUDP,
654
654
+
SourcePortStart: "123",
655
655
+
SourcePortEnd: "123",
656
656
+
Position: 3,
657
657
+
Comment: "Allow NTP replies",
658
658
+
},
659
659
+
{
650
660
Direction: upcloud.FirewallRuleDirectionIn,
651
661
Action: upcloud.FirewallRuleActionDrop,
652
652
-
Position: 3,
662
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
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
93
-
supported, online := probeRequestCrawl(relayURL)
94
94
+
supported, statusCode, online := probeRequestCrawl(relayURL)
94
95
if online {
95
96
markOnline()
96
97
}
97
97
-
if supported {
98
98
-
mu.Lock()
99
99
-
result.HasRequestCrawl = true
100
100
-
mu.Unlock()
101
101
-
}
98
98
+
mu.Lock()
99
99
+
result.HasRequestCrawl = supported
100
100
+
result.RequestCrawlStatus = statusCode
101
101
+
mu.Unlock()
102
102
}()
103
103
104
104
// Check host status
···
158
158
return result
159
159
}
160
160
161
161
-
// probeRequestCrawl checks if a relay supports the requestCrawl endpoint using a HEAD request.
162
162
-
// A 4xx response (e.g. 405 Method Not Allowed) means the endpoint exists.
163
163
-
// A 5xx or connection failure means it's broken or unsupported.
164
164
-
func probeRequestCrawl(relayURL string) (supported bool, online bool) {
161
161
+
// probeRequestCrawl checks if a relay supports the requestCrawl endpoint by POSTing
162
162
+
// an empty hostname. Returns (supported, statusCode, online):
163
163
+
// - 400 = endpoint exists and accepts unauthenticated crawls (supported=true)
164
164
+
// - 401/403 = endpoint exists but requires auth (supported=false)
165
165
+
// - 5xx = endpoint is broken (supported=false)
166
166
+
// - connection error = relay offline (online=false)
167
167
+
func probeRequestCrawl(relayURL string) (supported bool, statusCode int, online bool) {
165
168
client := &http.Client{Timeout: 5 * time.Second}
166
166
-
req, err := http.NewRequest("HEAD", relayURL+SyncRequestCrawl, nil)
169
169
+
body := bytes.NewReader([]byte(`{"hostname":""}`))
170
170
+
req, err := http.NewRequest("POST", relayURL+SyncRequestCrawl, body)
167
171
if err != nil {
168
168
-
return false, false
172
172
+
return false, 0, false
169
173
}
174
174
+
req.Header.Set("Content-Type", "application/json")
170
175
171
176
resp, err := client.Do(req)
172
177
if err != nil {
173
173
-
return false, false
178
178
+
return false, 0, false
174
179
}
175
180
defer resp.Body.Close()
176
181
177
177
-
// Any HTTP response means the relay is online.
178
178
-
// 4xx (typically 405 Method Not Allowed) = endpoint exists.
179
179
-
// 5xx = endpoint is broken.
180
180
-
return resp.StatusCode >= 400 && resp.StatusCode < 500, true
182
182
+
// 400 = endpoint exists, accepts unauthenticated requests (empty hostname rejected as expected)
183
183
+
// 401/403 = endpoint exists but requires authentication
184
184
+
// 5xx = endpoint is broken
185
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
220
+
case SyncRequestCrawl:
221
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
242
+
if !status.HasRequestCrawl {
243
243
+
t.Error("expected HasRequestCrawl = true")
244
244
+
}
245
245
+
if status.RequestCrawlStatus != http.StatusBadRequest {
246
246
+
t.Errorf("RequestCrawlStatus = %d, want %d", status.RequestCrawlStatus, http.StatusBadRequest)
247
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
268
+
case SyncRequestCrawl:
269
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
316
+
case SyncRequestCrawl:
317
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
342
+
343
343
+
func TestCheckRelayStatus_AuthRequired(t *testing.T) {
344
344
+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
345
345
+
switch r.URL.Path {
346
346
+
case SyncRequestCrawl:
347
347
+
w.WriteHeader(http.StatusForbidden) // auth required
348
348
+
case SyncGetHostStatus:
349
349
+
json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true})
350
350
+
case SyncGetRepoStatus:
351
351
+
json.NewEncoder(w).Encode(RepoStatus{DID: "did:web:hold.example.com", Active: true, Rev: "r1"})
352
352
+
case SyncListReposByCollection:
353
353
+
json.NewEncoder(w).Encode(map[string]any{"repos": []any{}})
354
354
+
default:
355
355
+
w.WriteHeader(http.StatusNotFound)
356
356
+
}
357
357
+
}))
358
358
+
defer srv.Close()
359
359
+
360
360
+
status := CheckRelayStatus(srv.URL, "hold.example.com", "did:web:hold.example.com")
361
361
+
362
362
+
if !status.Online {
363
363
+
t.Error("expected Online = true")
364
364
+
}
365
365
+
if status.HasRequestCrawl {
366
366
+
t.Error("expected HasRequestCrawl = false (auth required)")
367
367
+
}
368
368
+
if status.RequestCrawlStatus != http.StatusForbidden {
369
369
+
t.Errorf("RequestCrawlStatus = %d, want %d", status.RequestCrawlStatus, http.StatusForbidden)
370
370
+
}
371
371
+
if !status.HasListReposByCollection {
372
372
+
t.Error("expected HasListReposByCollection = true")
373
373
+
}
374
374
+
if status.RepoStatus == nil || !status.RepoStatus.Active {
375
375
+
t.Error("expected RepoStatus to be active")
376
376
+
}
377
377
+
}
+2
pkg/hold/admin/handlers_relays.go
···
22
22
Online bool
23
23
Error string
24
24
HasRequestCrawl bool
25
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
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
25
-
{{if .HasRequestCrawl}}<span class="badge badge-ghost badge-sm">requestCrawl</span>{{end}}
25
25
+
{{if .HasRequestCrawl}}
26
26
+
<span class="badge badge-ghost badge-sm">requestCrawl</span>
27
27
+
{{else if or (eq .RequestCrawlStatus 401) (eq .RequestCrawlStatus 403)}}
28
28
+
<span class="badge badge-warning badge-sm">requestCrawl (auth required)</span>
29
29
+
{{else if ge .RequestCrawlStatus 500}}
30
30
+
<span class="badge badge-error badge-sm">requestCrawl ({{.RequestCrawlStatus}})</span>
31
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>