The dual-faced felis-api: internal (service) and external (public/app/admin) routes behind a Zero-Trust guard. Includes the access domain (whitelist, ban, and LuckPerms permission/group control over the owner-gated RCON path), the modpack submission endpoints, and the admin-tier SysAdmin fleet read. Structured access fields are charset-validated before assembly so no field can splice a second RCON command.
101 lines
4.2 KiB
Go
101 lines
4.2 KiB
Go
package api
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// adminAPIWithBuildLogs is adminAPI plus a build-log streamer, so the §16
|
|
// build-log handler is exercised against a fake (the real K8sBuildLogStreamer's
|
|
// pod selection + pods/log follow are integration-only). A bare fakeBuilder
|
|
// satisfies the admin gate's interface dependency.
|
|
func adminAPIWithBuildLogs(streamer LogStreamer) *API {
|
|
api := adminAPI(&fakeBuilder{})
|
|
api.BuildLogs = streamer
|
|
return api
|
|
}
|
|
|
|
// A valid build id streams the build Pod log as SSE and forwards the id to the
|
|
// build-namespace streamer (spec §16, §416 日志流复用 §8). A finite source (one
|
|
// line then EOF) lets the relay return without blocking on the heartbeat.
|
|
func TestBuildLogsStreamsForAdmin(t *testing.T) {
|
|
streamer := &fakeLogStreamer{src: io.NopCloser(strings.NewReader("building image...\n"))}
|
|
api := adminAPIWithBuildLogs(streamer)
|
|
w := do(api.ExternalHandler(), "GET", "/api/v1/images/build/bld-1/logs", "", nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("code = %d, want 200 (%s)", w.Code, w.Body.String())
|
|
}
|
|
if ct := w.Header().Get("Content-Type"); ct != "text/event-stream" {
|
|
t.Errorf("Content-Type = %q, want text/event-stream", ct)
|
|
}
|
|
if streamer.calls != 1 {
|
|
t.Fatalf("streamer called %d times, want 1", streamer.calls)
|
|
}
|
|
if streamer.gotName != "bld-1" {
|
|
t.Errorf("streamer got id %q, want bld-1 (the build id must reach the streamer)", streamer.gotName)
|
|
}
|
|
if !strings.Contains(w.Body.String(), "data: building image...\n\n") {
|
|
t.Errorf("body missing the streamed log line as an SSE data event: %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// A malformed build id is rejected up front (400) and never reaches the streamer:
|
|
// the id becomes a label-selector value, so a crafted id (here a comma + '=' that
|
|
// would inject a second selector requirement) must be refused before selection.
|
|
func TestBuildLogsRejectsMalformedID(t *testing.T) {
|
|
streamer := &fakeLogStreamer{src: io.NopCloser(strings.NewReader("x\n"))}
|
|
api := adminAPIWithBuildLogs(streamer)
|
|
w := do(api.ExternalHandler(), "GET", "/api/v1/images/build/bld-1,evil=x/logs", "", nil)
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("code = %d, want 400 (%s)", w.Code, w.Body.String())
|
|
}
|
|
if decodeErr(t, w) != "bad_request" {
|
|
t.Errorf("error code = %q, want bad_request", decodeErr(t, w))
|
|
}
|
|
if streamer.calls != 0 {
|
|
t.Errorf("streamer was called %d times for a malformed id; must be 0 (no selector built)", streamer.calls)
|
|
}
|
|
}
|
|
|
|
// With no BuildLogs streamer configured, the route reports 503 — but only after
|
|
// the admin gate and id validation, so the boundary is still enforced.
|
|
func TestBuildLogsUnconfiguredIs503(t *testing.T) {
|
|
api := adminAPI(&fakeBuilder{}) // BuildLogs deliberately left nil
|
|
w := do(api.ExternalHandler(), "GET", "/api/v1/images/build/bld-1/logs", "", nil)
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("code = %d, want 503 (%s)", w.Code, w.Body.String())
|
|
}
|
|
if decodeErr(t, w) != "build_logs_unavailable" {
|
|
t.Errorf("error code = %q, want build_logs_unavailable", decodeErr(t, w))
|
|
}
|
|
}
|
|
|
|
// No build Pod for the id (not scheduled yet, or GC'd after completion) is 404 —
|
|
// distinct from the 409 the §8 server console returns for a stopped server, since
|
|
// a build's pod absence is "not found", not "not running".
|
|
func TestBuildLogsNotFoundIs404(t *testing.T) {
|
|
api := adminAPIWithBuildLogs(&fakeLogStreamer{err: ErrNotFound})
|
|
w := do(api.ExternalHandler(), "GET", "/api/v1/images/build/bld-1/logs", "", nil)
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("code = %d, want 404 (%s)", w.Code, w.Body.String())
|
|
}
|
|
if decodeErr(t, w) != "not_found" {
|
|
t.Errorf("error code = %q, want not_found", decodeErr(t, w))
|
|
}
|
|
}
|
|
|
|
// A pod that exists but whose log stream cannot be opened (e.g. kaniko's image is
|
|
// still pulling, container Waiting) is a transient 503 the client retries.
|
|
func TestBuildLogsUnavailableIs503(t *testing.T) {
|
|
api := adminAPIWithBuildLogs(&fakeLogStreamer{err: ErrConsoleUnavailable})
|
|
w := do(api.ExternalHandler(), "GET", "/api/v1/images/build/bld-1/logs", "", nil)
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("code = %d, want 503 (%s)", w.Code, w.Body.String())
|
|
}
|
|
if decodeErr(t, w) != "build_logs_unavailable" {
|
|
t.Errorf("error code = %q, want build_logs_unavailable", decodeErr(t, w))
|
|
}
|
|
}
|