mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
feat: add authenticated supervisor drain
This commit is contained in:
+1
-1
@@ -1218,7 +1218,7 @@ the local/CI/community transport, not a silent production fallback.
|
||||
| 8.33 `[D:8.26,8.32]` | On-demand-only live capacity and measured N+1: loss of largest node leaves two Ready slots plus headroom for surviving Allocated matches | Interruptible nodes cannot receive live matches; forced node loss neither overloads survivors nor prevents the next allocation |
|
||||
| 8.34 `[D:8.28,8.29]` | Native x86_64 benchmark of boot-to-process-ready and assignment-ready, p99 CPU/RSS/network and 60 Hz ticks; limits/node cap with 30% headroom | Measurements replace old estimates and certify density with no tick backlog |
|
||||
| 8.35 `[D:8.17,8.19,8.20,8.30,8.31]` | **IN PROGRESS.** Pure Go initial-connect policy decides ranked 30 s no-show cancellation with abandon ladder and casual 60 s bot start only when each team has a human; empty-team casual allocations cancel | `server/domain/noshow.go` covers wait/deadline boundaries, deterministic no-show/innocent ordering, ranked cooldown history and no pre-live rating action; persistent ticket restoration, allocation shutdown, bot spawn and live integration remain |
|
||||
| 8.36 `[D:8.10,8.25,8.28,8.30]` | Go PID-1 supervisor traps TERM and authenticates localhost drain; 300 s grace/285 s infrastructure abort; PDB + Agones-aware Fleet drain; planned releases never TERM Allocated pods | Rollout/rollback waits Allocated=0; TERM path is exercised; forced timeout is classified/refunded; unexpected node loss is not claimed graceful |
|
||||
| 8.36 `[D:8.10,8.25,8.28,8.30]` | **IN PROGRESS.** Supervisor exposes an authenticated localhost drain request boundary that never places the token in command arguments/logs | `server/supervisor/` covers bearer-token enforcement and rejection of missing drain credentials; TERM signal handling, 300 s/285 s lifecycle, PDB/Fleet drain and infrastructure-abort classification remain |
|
||||
| 8.37 `[D:8.5,8.10,8.25,8.26,8.31]` | Horizontally scaled primary control plane + warm standby, EU/NA fleets, RPO <=5 m/RTO <=30 m; signed result annotation/reconciliation preserves delivery outages | Restore/failover meet targets; live simulation continues; valid delayed result commits exactly once after recovery |
|
||||
| 8.38 `[D:7.7,8.26,8.36,8.37]` | Provider migration after Valve approves both providers' EU/NA POPs/certs and public UDP: restore, validate coordinator trust, switch allocations, drain old | Both geographies complete Hosted-SDR matches on new provider and no old-provider live match is terminated |
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ type Config struct {
|
||||
SDKBaseURL string
|
||||
ReadyURL string
|
||||
Transport string
|
||||
DrainURL string
|
||||
DrainToken string
|
||||
ReadyTimeout time.Duration
|
||||
PollInterval time.Duration
|
||||
HTTPClient *http.Client
|
||||
@@ -115,6 +117,29 @@ func (s *Supervisor) Wait() error {
|
||||
return s.cmd.Wait()
|
||||
}
|
||||
|
||||
// Drain asks the allocated Godot process to stop accepting new work. The
|
||||
// token is sent only over the configured localhost control endpoint and is
|
||||
// never placed in command arguments or logs.
|
||||
func (s *Supervisor) Drain(ctx context.Context) error {
|
||||
if s.config.DrainURL == "" || s.config.DrainToken == "" {
|
||||
return fmt.Errorf("authenticated drain endpoint is required")
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, s.config.DrainURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Authorization", "Bearer "+s.config.DrainToken)
|
||||
response, err := s.client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode/100 != 2 {
|
||||
return fmt.Errorf("drain endpoint returned %s", response.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Supervisor) assignedEndpoint(ctx context.Context) (int, string, error) {
|
||||
var server GameServer
|
||||
if err := s.sdkGet(ctx, "/gameserver", &server); err != nil {
|
||||
|
||||
@@ -111,3 +111,34 @@ func TestDirectModeDoesNotRequireAgonesReadiness(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrainRequiresAndUsesAuthenticatedLocalEndpoint(t *testing.T) {
|
||||
seenToken := ""
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/drain" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
seenToken = r.Header.Get("Authorization")
|
||||
if seenToken != "Bearer secret-token" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}))
|
||||
defer server.Close()
|
||||
s, err := New(Config{Command: []string{"/bin/sh", "-c", "exit 0"}, DrainURL: server.URL + "/drain", DrainToken: "secret-token"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Drain(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if seenToken != "Bearer secret-token" {
|
||||
t.Fatalf("unexpected drain token: %q", seenToken)
|
||||
}
|
||||
missing, _ := New(Config{Command: []string{"/bin/sh", "-c", "exit 0"}})
|
||||
if err := missing.Drain(context.Background()); err == nil {
|
||||
t.Fatal("unauthenticated drain was allowed")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user