upcloud provision fixes and relay tweaks

This commit is contained in:
Evan Jarrett
2026-03-01 20:52:41 -06:00
parent fcc5fa78bc
commit 11a8be1413
6 changed files with 91 additions and 19 deletions
+1
View File
@@ -18,6 +18,7 @@ done
export DEBIAN_FRONTEND=noninteractive
apt-get update && apt-get upgrade -y
apt-get install -y git gcc make curl libsqlite3-dev nodejs npm htop systemd-timesyncd
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
timedatectl set-ntp true
# Swap (for small instances)
+11 -1
View File
@@ -646,10 +646,20 @@ func createFirewallRules(ctx context.Context, svc *service.Service, serverUUID,
Position: 2,
Comment: "Allow private network",
},
{
Direction: upcloud.FirewallRuleDirectionIn,
Action: upcloud.FirewallRuleActionAccept,
Family: upcloud.IPAddressFamilyIPv4,
Protocol: upcloud.FirewallRuleProtocolUDP,
SourcePortStart: "123",
SourcePortEnd: "123",
Position: 3,
Comment: "Allow NTP replies",
},
{
Direction: upcloud.FirewallRuleDirectionIn,
Action: upcloud.FirewallRuleActionDrop,
Position: 3,
Position: 4,
Comment: "Drop all other inbound",
},
},
+22 -17
View File
@@ -67,6 +67,7 @@ type RelayStatus struct {
Online bool
Error string
HasRequestCrawl bool
RequestCrawlStatus int // HTTP status code from probe (400=open, 401/403=auth required, 5xx=error)
HasListReposByCollection bool
RepoStatus *RepoStatus
HostStatus *HostStatus
@@ -90,15 +91,14 @@ func CheckRelayStatus(relayURL, hostname, did string) *RelayStatus {
// Probe requestCrawl
go func() {
defer wg.Done()
supported, online := probeRequestCrawl(relayURL)
supported, statusCode, online := probeRequestCrawl(relayURL)
if online {
markOnline()
}
if supported {
mu.Lock()
result.HasRequestCrawl = true
mu.Unlock()
}
mu.Lock()
result.HasRequestCrawl = supported
result.RequestCrawlStatus = statusCode
mu.Unlock()
}()
// Check host status
@@ -158,26 +158,31 @@ func CheckRelayStatus(relayURL, hostname, did string) *RelayStatus {
return result
}
// probeRequestCrawl checks if a relay supports the requestCrawl endpoint using a HEAD request.
// A 4xx response (e.g. 405 Method Not Allowed) means the endpoint exists.
// A 5xx or connection failure means it's broken or unsupported.
func probeRequestCrawl(relayURL string) (supported bool, online bool) {
// probeRequestCrawl checks if a relay supports the requestCrawl endpoint by POSTing
// an empty hostname. Returns (supported, statusCode, online):
// - 400 = endpoint exists and accepts unauthenticated crawls (supported=true)
// - 401/403 = endpoint exists but requires auth (supported=false)
// - 5xx = endpoint is broken (supported=false)
// - connection error = relay offline (online=false)
func probeRequestCrawl(relayURL string) (supported bool, statusCode int, online bool) {
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("HEAD", relayURL+SyncRequestCrawl, nil)
body := bytes.NewReader([]byte(`{"hostname":""}`))
req, err := http.NewRequest("POST", relayURL+SyncRequestCrawl, body)
if err != nil {
return false, false
return false, 0, false
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return false, false
return false, 0, false
}
defer resp.Body.Close()
// Any HTTP response means the relay is online.
// 4xx (typically 405 Method Not Allowed) = endpoint exists.
// 5xx = endpoint is broken.
return resp.StatusCode >= 400 && resp.StatusCode < 500, true
// 400 = endpoint exists, accepts unauthenticated requests (empty hostname rejected as expected)
// 401/403 = endpoint exists but requires authentication
// 5xx = endpoint is broken
return resp.StatusCode == http.StatusBadRequest, resp.StatusCode, true
}
// probeListReposByCollection checks if a relay supports the listReposByCollection endpoint.
+48
View File
@@ -217,6 +217,8 @@ func TestCheckHostStatus(t *testing.T) {
func TestCheckRelayStatus_AllEndpointsSucceed(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case SyncRequestCrawl:
w.WriteHeader(http.StatusBadRequest) // empty hostname = 400
case SyncGetHostStatus:
json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true})
case SyncGetRepoStatus:
@@ -237,6 +239,12 @@ func TestCheckRelayStatus_AllEndpointsSucceed(t *testing.T) {
if status.Error != "" {
t.Errorf("expected no error, got %q", status.Error)
}
if !status.HasRequestCrawl {
t.Error("expected HasRequestCrawl = true")
}
if status.RequestCrawlStatus != http.StatusBadRequest {
t.Errorf("RequestCrawlStatus = %d, want %d", status.RequestCrawlStatus, http.StatusBadRequest)
}
if !status.HasListReposByCollection {
t.Error("expected HasListReposByCollection = true")
}
@@ -257,6 +265,8 @@ func TestCheckRelayStatus_AllEndpointsSucceed(t *testing.T) {
func TestCheckRelayStatus_OnlineButUnknownHost(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case SyncRequestCrawl:
w.WriteHeader(http.StatusBadRequest) // empty hostname = 400
case SyncGetHostStatus:
w.WriteHeader(http.StatusBadRequest) // relay doesn't know this host
case SyncGetRepoStatus:
@@ -303,6 +313,8 @@ func TestCheckRelayStatus_Offline(t *testing.T) {
func TestCheckRelayStatus_NoListReposByCollection(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case SyncRequestCrawl:
w.WriteHeader(http.StatusBadRequest) // empty hostname = 400
case SyncGetHostStatus:
json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true})
case SyncGetRepoStatus:
@@ -327,3 +339,39 @@ func TestCheckRelayStatus_NoListReposByCollection(t *testing.T) {
t.Error("expected RepoStatus to be active")
}
}
func TestCheckRelayStatus_AuthRequired(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case SyncRequestCrawl:
w.WriteHeader(http.StatusForbidden) // auth required
case SyncGetHostStatus:
json.NewEncoder(w).Encode(HostStatus{Hostname: "hold.example.com", Active: true})
case SyncGetRepoStatus:
json.NewEncoder(w).Encode(RepoStatus{DID: "did:web:hold.example.com", Active: true, Rev: "r1"})
case SyncListReposByCollection:
json.NewEncoder(w).Encode(map[string]any{"repos": []any{}})
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer srv.Close()
status := CheckRelayStatus(srv.URL, "hold.example.com", "did:web:hold.example.com")
if !status.Online {
t.Error("expected Online = true")
}
if status.HasRequestCrawl {
t.Error("expected HasRequestCrawl = false (auth required)")
}
if status.RequestCrawlStatus != http.StatusForbidden {
t.Errorf("RequestCrawlStatus = %d, want %d", status.RequestCrawlStatus, http.StatusForbidden)
}
if !status.HasListReposByCollection {
t.Error("expected HasListReposByCollection = true")
}
if status.RepoStatus == nil || !status.RepoStatus.Active {
t.Error("expected RepoStatus to be active")
}
}
+2
View File
@@ -22,6 +22,7 @@ type RelayStatusView struct {
Online bool
Error string
HasRequestCrawl bool
RequestCrawlStatus int
HasListReposByCollection bool
RepoStatus *atproto.RepoStatus
HostStatus *atproto.HostStatus
@@ -77,6 +78,7 @@ func (ui *AdminUI) handleRelayStatus(w http.ResponseWriter, r *http.Request) {
Online: status.Online,
Error: status.Error,
HasRequestCrawl: status.HasRequestCrawl,
RequestCrawlStatus: status.RequestCrawlStatus,
HasListReposByCollection: status.HasListReposByCollection,
RepoStatus: status.RepoStatus,
HostStatus: status.HostStatus,
@@ -22,7 +22,13 @@
<td>
<div class="flex flex-wrap gap-1">
{{if .Online}}
{{if .HasRequestCrawl}}<span class="badge badge-ghost badge-sm">requestCrawl</span>{{end}}
{{if .HasRequestCrawl}}
<span class="badge badge-ghost badge-sm">requestCrawl</span>
{{else if or (eq .RequestCrawlStatus 401) (eq .RequestCrawlStatus 403)}}
<span class="badge badge-warning badge-sm">requestCrawl (auth required)</span>
{{else if ge .RequestCrawlStatus 500}}
<span class="badge badge-error badge-sm">requestCrawl ({{.RequestCrawlStatus}})</span>
{{end}}
{{if .HasListReposByCollection}}<span class="badge badge-ghost badge-sm">listReposByCollection</span>{{end}}
{{else}}
<span class="text-base-content/30 text-sm">-</span>