mirror of
https://github.com/livekit/livekit.git
synced 2026-07-28 07:49:30 +00:00
add mock for testing region pins with API (#4691)
we'll ensure that clients support redirection when making an API to an unpinned region
This commit is contained in:
@@ -67,6 +67,7 @@ field — for normal behavior. Every field is optional:
|
||||
| `response` | — | the response message for the called method (a JSON object, protojson-shaped); replaces the populated default, giving full control over the returned payload. |
|
||||
| `skipAuth` | `false` | `true` disables permission enforcement for the request (use for tests that aren't about authz, e.g. failover tests with a placeholder token). |
|
||||
| `sipStatus` | — | fail a SIP dial method (`CreateSIPParticipant`/`TransferSIPParticipant`) with a SIP status, e.g. `{"code":486,"status":"Busy Here"}` (`status` optional). The Twirp error code and `sip_status_code`/`sip_status`/`error_details` metadata are derived from it exactly as the real server does. Composes with `delayMs` to simulate "ring, then fail". |
|
||||
| `pinnedRegions` | — | array of region **names** the project is pinned to (its **allowed** regions), e.g. `["region-1"]`, mirroring cloud's per-project `PinnedRegions` (region names, not indices). When non-empty a pin is in effect: any region whose name is **not** listed rejects the request with **HTTP 451** (a region-pin violation), and `GET /settings/regions` returns **only** the listed regions. The SDK is expected to see the 451, re-fetch `/settings/regions`, and retry against an allowed region — the 451 carries no region hint (its body is the middleware's plain text, not a Twirp error). This redirect is always active in clients; it can't be disabled. Use a name no listener advertises (e.g. `["region-99"]`) to model a pinned region that's unreachable. Names are the mock's region names (`region-0`, `region-1`, …); contrast `failRegions`, which addresses listeners by index. |
|
||||
|
||||
Example: `X-Lk-Mock: {"skipAuth":true,"failRegions":[0],"failStatus":400}`
|
||||
|
||||
|
||||
@@ -30,6 +30,16 @@ const (
|
||||
headerRegion = "X-Lk-Mock-Region"
|
||||
)
|
||||
|
||||
const (
|
||||
// regionPinStatus is the HTTP status cloud middleware returns for a region-pin
|
||||
// violation — an API call reaching a region the project is not pinned to. It is
|
||||
// 451 (Unavailable For Legal Reasons); clients key off it to rediscover regions
|
||||
// and retry against an allowed one.
|
||||
regionPinStatus = http.StatusUnavailableForLegalReasons
|
||||
// regionPinMessage is the plain-text body the middleware writes with the 451.
|
||||
regionPinMessage = "project not allowed in this region."
|
||||
)
|
||||
|
||||
// Deprecated: the individual X-Lk-Mock-* control headers predate the unified
|
||||
// X-Lk-Mock JSON header. They are still honored for existing clients; new
|
||||
// clients should send X-Lk-Mock instead. When X-Lk-Mock is present its fields
|
||||
@@ -86,6 +96,18 @@ type mockConfig struct {
|
||||
// derived from it exactly as the real server does, so the SDK sees an
|
||||
// identical error. Composes with DelayMs to simulate "ring, then fail".
|
||||
SIPStatus *sipStatusConfig `json:"sipStatus,omitempty"`
|
||||
// PinnedRegions lists the region names a project is pinned to (its allowed
|
||||
// regions), mirroring cloud's per-project PinnedRegions — which are region names,
|
||||
// not indices. When non-empty a pin is in effect: any region whose name is NOT
|
||||
// listed rejects the request with HTTP 451 (see regionPinStatus), exactly as
|
||||
// cloud middleware turns away an API call reaching a region the project is not
|
||||
// pinned to, and GET /settings/regions returns only the listed regions. The
|
||||
// client is expected to see the 451, re-fetch /settings/regions, and retry
|
||||
// against an allowed region — the 451 carries no region hint. An empty/absent
|
||||
// value means "not pinned": every region serves and discovery returns all
|
||||
// regions. Names are this mock's region names (e.g. "region-0"); contrast
|
||||
// FailRegions, which addresses listeners by index.
|
||||
PinnedRegions []string `json:"pinnedRegions,omitempty"`
|
||||
|
||||
// legacyDelayMs is the sleep used by the deprecated "delay" fail mode. It is
|
||||
// populated only from the legacy X-Lk-Mock-Delay-Ms header, never from JSON.
|
||||
|
||||
@@ -17,6 +17,7 @@ package main
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -145,6 +146,14 @@ func (h *mockHandler) serveAPI(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// A project pinned to other regions is turned away by cloud middleware up front
|
||||
// — before the request is served and with no latency — when it reaches a region
|
||||
// it isn't pinned to. The check is by region name, as it is on the real server.
|
||||
if len(cfg.PinnedRegions) > 0 && !slices.Contains(cfg.PinnedRegions, h.regionName()) {
|
||||
h.failRegionPin(w)
|
||||
return
|
||||
}
|
||||
|
||||
// Delay before responding (success or failure). An explicit delayMs overrides
|
||||
// the method's natural latency — e.g. CreateSIPParticipant blocking until the
|
||||
// callee answers.
|
||||
@@ -211,6 +220,16 @@ func (h *mockHandler) failSIP(w http.ResponseWriter, cfg *mockConfig) {
|
||||
writeTwirpErr(w, xtwirp.ToError(st))
|
||||
}
|
||||
|
||||
// failRegionPin rejects the request with HTTP 451, mirroring cloud middleware
|
||||
// turning away an API call from a project pinned to a different region. The body
|
||||
// is the middleware's plain-text message (not a Twirp error): clients key off the
|
||||
// 451 status to rediscover regions and retry against an allowed one.
|
||||
func (h *mockHandler) failRegionPin(w http.ResponseWriter) {
|
||||
w.Header().Set(headerRegion, "")
|
||||
w.WriteHeader(regionPinStatus)
|
||||
_, _ = w.Write([]byte(regionPinMessage))
|
||||
}
|
||||
|
||||
// writeAPIResponse serves a populated, type-correct response for a known API
|
||||
// method. The response is the reflection-populated default unless the mock
|
||||
// config carries a `response` (protojson), which overrides it entirely. Content
|
||||
|
||||
+29
-1
@@ -112,7 +112,14 @@ func (h *mockHandler) handleRegions(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(cfg.RegionsStatus)
|
||||
return
|
||||
}
|
||||
body, err := protojson.Marshal(h.regions)
|
||||
regions := h.regions
|
||||
if len(cfg.PinnedRegions) > 0 {
|
||||
// A pinned project's discovery returns only its allowed regions, so the
|
||||
// client redirects straight to one after a 451 — mirroring cloud filtering
|
||||
// /settings/regions by the project's PinnedRegions.
|
||||
regions = pinnedRegionSettings(h.regions, cfg.PinnedRegions)
|
||||
}
|
||||
body, err := protojson.Marshal(regions)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
@@ -127,6 +134,27 @@ func (h *mockHandler) handleTwirp(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveAPI(w, r)
|
||||
}
|
||||
|
||||
// regionName is the advertised name of the region this listener represents.
|
||||
func (h *mockHandler) regionName() string {
|
||||
if h.regionIndex < len(h.regions.Regions) {
|
||||
return h.regions.Regions[h.regionIndex].Region
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// pinnedRegionSettings returns only the regions whose name appears in pinned,
|
||||
// mirroring how cloud filters /settings/regions to a project's PinnedRegions.
|
||||
// Names outside the region list are ignored.
|
||||
func pinnedRegionSettings(all *livekit.RegionSettings, pinned []string) *livekit.RegionSettings {
|
||||
out := &livekit.RegionSettings{}
|
||||
for _, region := range all.Regions {
|
||||
if slices.Contains(pinned, region.Region) {
|
||||
out.Regions = append(out.Regions, region)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *mockHandler) shouldFail(cfg *mockConfig) bool {
|
||||
return slices.Contains(cfg.FailRegions, h.regionIndex)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user