рабочий вариант, но скороть 10 МБит
build / backend (push) Has been cancelled
build / node-agent (push) Has been cancelled
build / worker (push) Has been cancelled

This commit is contained in:
2026-05-22 21:46:49 +03:00
parent 469fa0e860
commit 20d361a886
280 changed files with 954890 additions and 18524 deletions
@@ -77,14 +77,10 @@ func (d AdminRuntimeDispatcher) HandleFabricRequest(ctx context.Context, request
func allowedAdminRuntimeScope(scope string, serviceClass string) bool {
switch serviceClass {
case "platform_admin":
return scope == "platform"
case "cluster_admin":
return scope == "cluster"
case "organization_portal":
return scope == "organization"
case "user_portal":
return scope == "user" || scope == "organization"
case "admin-ingress":
return scope == "platform" || scope == "cluster"
case "public-ingress":
return scope == "organization" || scope == "user"
default:
return false
}
@@ -143,18 +139,22 @@ func (d AdminRuntimeDispatcher) manifest(request FabricRequest) map[string]any {
sections := []string{}
actions := []string{}
switch serviceClass {
case "platform_admin":
case "admin-ingress":
sections = []string{"clusters", "nodes", "roles", "fabric", "workloads", "audit"}
actions = []string{"read_platform_summary", "read_cluster_summaries", "read_node_status"}
case "cluster_admin":
sections = []string{"cluster", "nodes", "fabric", "workloads", "audit"}
actions = []string{"read_cluster_summary", "read_node_status"}
case "organization_portal":
if request.Scope == "cluster" {
sections = []string{"cluster", "nodes", "fabric", "workloads", "audit"}
actions = []string{"read_cluster_summary", "read_node_status"}
} else {
actions = []string{"read_platform_summary", "read_cluster_summaries", "read_node_status"}
}
case "public-ingress":
sections = []string{"organization", "sessions", "resources", "audit"}
actions = []string{"read_organization_summary", "read_sessions"}
case "user_portal":
sections = []string{"profile", "sessions", "resources"}
actions = []string{"read_profile", "read_sessions"}
if request.Scope == "user" {
sections = []string{"profile", "sessions", "resources"}
actions = []string{"read_profile", "read_sessions"}
} else {
actions = []string{"read_organization_summary", "read_sessions"}
}
default:
sections = []string{"status"}
actions = []string{"read_status"}
@@ -14,7 +14,7 @@ func TestAdminRuntimeDispatcherReturnsHealthAndManifest(t *testing.T) {
Method: http.MethodGet,
Path: "/readyz",
Scope: "platform",
ServiceClass: "platform_admin",
ServiceClass: "admin-ingress",
})
if err != nil {
t.Fatalf("health: %v", err)
@@ -25,9 +25,9 @@ func TestAdminRuntimeDispatcherReturnsHealthAndManifest(t *testing.T) {
manifest, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/platform-admin/ui-manifest",
Path: "/admin/ui-manifest",
Scope: "platform",
ServiceClass: "platform_admin",
ServiceClass: "admin-ingress",
})
if err != nil {
t.Fatalf("manifest: %v", err)
@@ -51,9 +51,9 @@ func TestAdminRuntimeDispatcherBlocksMutationsAndUnknownProjection(t *testing.T)
mutation, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodPost,
Path: "/platform-admin/nodes",
Path: "/admin/nodes",
Scope: "platform",
ServiceClass: "platform_admin",
ServiceClass: "admin-ingress",
})
if err != nil {
t.Fatalf("mutation: %v", err)
@@ -68,9 +68,9 @@ func TestAdminRuntimeDispatcherBlocksMutationsAndUnknownProjection(t *testing.T)
projection, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/platform-admin/nodes",
Path: "/admin/nodes",
Scope: "platform",
ServiceClass: "platform_admin",
ServiceClass: "admin-ingress",
})
if err != nil {
t.Fatalf("projection: %v", err)
@@ -88,9 +88,9 @@ func TestAdminRuntimeDispatcherRejectsInvalidScopeClassPair(t *testing.T) {
dispatcher := AdminRuntimeDispatcher{ProjectionClient: &recordingProjectionClient{}, Now: fixedEnvelopeNow}
response, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/platform-admin/ui-manifest",
Path: "/admin/ui-manifest",
Scope: "organization",
ServiceClass: "platform_admin",
ServiceClass: "admin-ingress",
})
if err != nil {
t.Fatalf("projection: %v", err)
@@ -118,11 +118,11 @@ func TestAdminRuntimeDispatcherUsesControlAPIProjectionClientForReadRequests(t *
response, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/platform-admin/nodes",
Path: "/admin/nodes",
Query: "limit=10",
Host: "admin.example.test",
Scope: "platform",
ServiceClass: "platform_admin",
ServiceClass: "admin-ingress",
})
if err != nil {
t.Fatalf("projection: %v", err)
@@ -133,10 +133,10 @@ func TestAdminRuntimeDispatcherUsesControlAPIProjectionClientForReadRequests(t *
string(response.Body) != `{"schema_version":"control.projection.v1","ok":true}` {
t.Fatalf("response = %+v body=%s", response, string(response.Body))
}
if client.request.Path != "/platform-admin/nodes" ||
if client.request.Path != "/admin/nodes" ||
client.request.Query != "limit=10" ||
client.request.Scope != "platform" ||
client.request.ServiceClass != "platform_admin" {
client.request.ServiceClass != "admin-ingress" {
t.Fatalf("request = %+v", client.request)
}
}
@@ -145,9 +145,9 @@ func TestAdminRuntimeDispatcherReportsProjectionClientFailure(t *testing.T) {
dispatcher := AdminRuntimeDispatcher{ProjectionClient: failingProjectionClient{}, Now: fixedEnvelopeNow}
response, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/platform-admin/nodes",
Path: "/admin/nodes",
Scope: "platform",
ServiceClass: "platform_admin",
ServiceClass: "admin-ingress",
})
if err != nil {
t.Fatalf("projection: %v", err)
@@ -175,9 +175,9 @@ func TestAdminRuntimeDispatcherRejectsInvalidProjectionResponseSchema(t *testing
}
response, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/platform-admin/nodes",
Path: "/admin/nodes",
Scope: "platform",
ServiceClass: "platform_admin",
ServiceClass: "admin-ingress",
})
if err != nil {
t.Fatalf("projection: %v", err)
@@ -13,7 +13,6 @@ import (
type ListenerConfig struct {
RuntimeConfig
HTTPAddr string
HTTPSAddr string
TLSCertFile string
TLSKeyFile string
@@ -23,9 +22,7 @@ type ListenerConfig struct {
type ListenerStatus struct {
SchemaVersion string `json:"schema_version"`
Running bool `json:"running"`
HTTPRunning bool `json:"http_running"`
HTTPSRunning bool `json:"https_running"`
HTTPAddr string `json:"http_addr,omitempty"`
HTTPSAddr string `json:"https_addr,omitempty"`
Reason string `json:"reason,omitempty"`
Errors []string `json:"errors,omitempty"`
@@ -34,7 +31,6 @@ type ListenerStatus struct {
type Manager struct {
mu sync.Mutex
http *http.Server
https *http.Server
status ListenerStatus
now func() time.Time
@@ -56,19 +52,9 @@ func (m *Manager) Apply(ctx context.Context, cfg ListenerConfig) ListenerStatus
ObservedAt: m.observedAt(),
}
errorsOut := []string{}
if strings.TrimSpace(cfg.HTTPAddr) == "" {
cfg.HTTPAddr = ":80"
}
if strings.TrimSpace(cfg.HTTPSAddr) == "" {
cfg.HTTPSAddr = ":443"
}
if server, addr, err := startHTTPServer(ctx, cfg.HTTPAddr, runtime.HTTPHandler()); err == nil {
m.http = server
status.HTTPRunning = true
status.HTTPAddr = addr
} else {
errorsOut = append(errorsOut, "http:"+err.Error())
}
if cfg.TLSCertFile == "" || cfg.TLSKeyFile == "" {
errorsOut = append(errorsOut, "https:tls_cert_file_and_key_file_required")
} else if server, addr, err := startHTTPSServer(ctx, cfg.HTTPSAddr, cfg.TLSCertFile, cfg.TLSKeyFile, runtime.HTTPSHandler()); err == nil {
@@ -78,7 +64,7 @@ func (m *Manager) Apply(ctx context.Context, cfg ListenerConfig) ListenerStatus
} else {
errorsOut = append(errorsOut, "https:"+err.Error())
}
status.Running = status.HTTPRunning || status.HTTPSRunning
status.Running = status.HTTPSRunning
if len(errorsOut) > 0 {
status.Errors = errorsOut
if status.Running {
@@ -118,10 +104,6 @@ func (m *Manager) Status() ListenerStatus {
func (m *Manager) stopLocked(ctx context.Context) error {
var out error
if m.http != nil {
out = errors.Join(out, m.http.Shutdown(ctx))
m.http = nil
}
if m.https != nil {
out = errors.Join(out, m.https.Shutdown(ctx))
m.https = nil
@@ -137,24 +119,6 @@ func (m *Manager) observedAt() string {
return now.Format(time.RFC3339Nano)
}
func startHTTPServer(ctx context.Context, addr string, handler http.Handler) (*http.Server, string, error) {
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, "", err
}
server := &http.Server{Handler: handler, ReadHeaderTimeout: 5 * time.Second}
go func() {
<-ctx.Done()
_ = server.Shutdown(context.Background())
}()
go func() {
if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
_ = server.Close()
}
}()
return server, listener.Addr().String(), nil
}
func startHTTPSServer(ctx context.Context, addr, certFile, keyFile string, handler http.Handler) (*http.Server, string, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
@@ -8,7 +8,6 @@ import (
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net/http"
"os"
"path/filepath"
"strings"
@@ -16,37 +15,6 @@ import (
"time"
)
func TestManagerStartsHTTPRedirectAndStops(t *testing.T) {
manager := NewManager()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
status := manager.Apply(ctx, ListenerConfig{
RuntimeConfig: RuntimeConfig{ServiceType: "admin-ingress", Scope: "platform", ServiceClasses: []string{"platform_admin"}},
HTTPAddr: "127.0.0.1:0",
HTTPSAddr: "127.0.0.1:0",
})
if !status.HTTPRunning || status.HTTPSRunning || !status.Running || status.HTTPAddr == "" {
t.Fatalf("status = %+v", status)
}
if status.Reason != "partial" || !containsError(status.Errors, "https:tls_cert_file_and_key_file_required") {
t.Fatalf("status = %+v", status)
}
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
resp, err := client.Get("http://" + status.HTTPAddr + "/cluster-admin")
if err != nil {
t.Fatalf("http get: %v", err)
}
_ = resp.Body.Close()
if resp.StatusCode != http.StatusPermanentRedirect {
t.Fatalf("status = %d", resp.StatusCode)
}
stopped := manager.Stop(context.Background())
if stopped.Running || stopped.Reason != "stopped" {
t.Fatalf("stopped = %+v", stopped)
}
}
func TestManagerStartsHTTPSWhenCertificateProvided(t *testing.T) {
dir := t.TempDir()
certFile, keyFile := writeSelfSignedCert(t, dir)
@@ -56,12 +24,29 @@ func TestManagerStartsHTTPSWhenCertificateProvided(t *testing.T) {
status := manager.Apply(ctx, ListenerConfig{
RuntimeConfig: RuntimeConfig{ServiceType: "admin-ingress", Scope: "platform", ServiceClasses: []string{"platform_admin"}},
HTTPAddr: "127.0.0.1:0",
HTTPSAddr: "127.0.0.1:0",
TLSCertFile: certFile,
TLSKeyFile: keyFile,
})
if !status.HTTPRunning || !status.HTTPSRunning || status.HTTPAddr == "" || status.HTTPSAddr == "" || len(status.Errors) != 0 {
if !status.HTTPSRunning || !status.Running || status.HTTPSAddr == "" || len(status.Errors) != 0 {
t.Fatalf("status = %+v", status)
}
}
func TestManagerDoesNotStartHTTPWithoutExplicitAddress(t *testing.T) {
dir := t.TempDir()
certFile, keyFile := writeSelfSignedCert(t, dir)
manager := NewManager()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
status := manager.Apply(ctx, ListenerConfig{
RuntimeConfig: RuntimeConfig{ServiceType: "admin-ingress", Scope: "platform", ServiceClasses: []string{"platform_admin"}},
HTTPSAddr: "127.0.0.1:0",
TLSCertFile: certFile,
TLSKeyFile: keyFile,
})
if !status.HTTPSRunning || !status.Running || status.HTTPSAddr == "" || len(status.Errors) != 0 {
t.Fatalf("status = %+v", status)
}
}
@@ -14,7 +14,6 @@ type RuntimeConfig struct {
Scope string
ServiceClasses []string
TLSMode string
HTTPPort int
HTTPSPort int
}
@@ -59,23 +58,6 @@ type Response struct {
ObservedAt string `json:"observed_at"`
}
func (r Runtime) HTTPHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if strings.HasPrefix(req.URL.Path, "/.well-known/acme-challenge/") {
writeJSON(w, http.StatusNotFound, r.response("not_found", "acme_challenge_backend_not_configured", ""))
return
}
if req.URL.Path == "/healthz" || req.URL.Path == "/readyz" {
writeJSON(w, http.StatusOK, r.response("ready", "http_redirect_runtime_ready", ""))
return
}
target := "https://" + req.Host + req.URL.RequestURI()
w.Header().Set("Location", target)
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusPermanentRedirect)
})
}
func (r Runtime) HTTPSHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/healthz" || req.URL.Path == "/readyz" {
@@ -98,7 +80,7 @@ func (r Runtime) HTTPSHandler() http.Handler {
writeJSON(w, http.StatusNotImplemented, r.response("blocked", "fabric_service_channel_binding_not_implemented", serviceClass))
return
}
scope := scopeForServiceClass(serviceClass, r.Config.Scope)
scope := scopeForServiceClass(serviceClass, req.URL.Path, r.Config.Scope)
body, err := io.ReadAll(http.MaxBytesReader(w, req.Body, 1<<20))
if err != nil {
writeJSON(w, http.StatusRequestEntityTooLarge, r.response("blocked", "request_body_too_large", serviceClass))
@@ -146,32 +128,38 @@ func (r Runtime) response(status, reason, serviceClass string) Response {
}
}
func scopeForServiceClass(serviceClass string, fallback string) string {
func scopeForServiceClass(serviceClass string, path string, fallback string) string {
path = strings.Trim(strings.ToLower(path), "/")
switch strings.TrimSpace(serviceClass) {
case "platform_admin":
return "platform"
case "cluster_admin":
return "cluster"
case "organization_portal":
return "organization"
case "user_portal":
return "user"
case "admin-ingress":
if strings.HasPrefix(path, "clusters/") {
return "cluster"
}
return firstNonEmpty(strings.TrimSpace(fallback), "platform")
case "public-ingress":
if strings.HasPrefix(path, "users/") {
return "user"
}
return firstNonEmpty(strings.TrimSpace(fallback), "organization")
default:
return strings.TrimSpace(fallback)
}
}
func firstNonEmpty(value string, fallback string) string {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
return strings.TrimSpace(fallback)
}
func serviceClassFromPath(path string) string {
path = strings.Trim(strings.ToLower(path), "/")
switch {
case strings.HasPrefix(path, "platform-admin"):
return "platform_admin"
case strings.HasPrefix(path, "cluster-admin"):
return "cluster_admin"
case strings.HasPrefix(path, "organizations/"):
return "organization_portal"
case strings.HasPrefix(path, "users/"):
return "user_portal"
case strings.HasPrefix(path, "admin/"), strings.HasPrefix(path, "platform/"), strings.HasPrefix(path, "clusters/"):
return "admin-ingress"
case strings.HasPrefix(path, "public/"), strings.HasPrefix(path, "organizations/"), strings.HasPrefix(path, "users/"):
return "public-ingress"
default:
return ""
}
@@ -10,31 +10,16 @@ import (
"time"
)
func TestHTTPHandlerRedirectsToHTTPS(t *testing.T) {
runtime := Runtime{Config: RuntimeConfig{ServiceType: "admin-ingress", Scope: "platform"}}
req := httptest.NewRequest(http.MethodGet, "http://admin.example.test/cluster-admin/dashboard?x=1", nil)
rec := httptest.NewRecorder()
runtime.HTTPHandler().ServeHTTP(rec, req)
if rec.Code != http.StatusPermanentRedirect {
t.Fatalf("status = %d", rec.Code)
}
if rec.Header().Get("Location") != "https://admin.example.test/cluster-admin/dashboard?x=1" {
t.Fatalf("Location = %q", rec.Header().Get("Location"))
}
}
func TestHTTPSHandlerBlocksUnknownServiceClass(t *testing.T) {
runtime := Runtime{
Config: RuntimeConfig{
ServiceType: "public-ingress",
Scope: "organization",
ServiceClasses: []string{"organization_portal", "user_portal"},
ServiceClasses: []string{"public-ingress", "public-ingress"},
},
Now: fixedNow,
}
req := httptest.NewRequest(http.MethodGet, "https://org.example.test/platform-admin/root", nil)
req := httptest.NewRequest(http.MethodGet, "https://org.example.test/admin/root", nil)
rec := httptest.NewRecorder()
runtime.HTTPSHandler().ServeHTTP(rec, req)
@@ -46,7 +31,7 @@ func TestHTTPSHandlerBlocksUnknownServiceClass(t *testing.T) {
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload.Reason != "service_class_not_allowed" || payload.ServiceClass != "platform_admin" || payload.Scope != "organization" {
if payload.Reason != "service_class_not_allowed" || payload.ServiceClass != "admin-ingress" || payload.Scope != "organization" {
t.Fatalf("payload = %+v", payload)
}
}
@@ -56,11 +41,11 @@ func TestHTTPSHandlerRequiresFabricServiceChannelBinding(t *testing.T) {
Config: RuntimeConfig{
ServiceType: "admin-ingress",
Scope: "platform",
ServiceClasses: []string{"platform_admin", "cluster_admin"},
ServiceClasses: []string{"admin-ingress", "admin-ingress"},
},
Now: fixedNow,
}
req := httptest.NewRequest(http.MethodPost, "https://admin.example.test/platform-admin/root", nil)
req := httptest.NewRequest(http.MethodPost, "https://admin.example.test/admin/root", nil)
rec := httptest.NewRecorder()
runtime.HTTPSHandler().ServeHTTP(rec, req)
@@ -73,7 +58,7 @@ func TestHTTPSHandlerRequiresFabricServiceChannelBinding(t *testing.T) {
t.Fatalf("decode response: %v", err)
}
if payload.Reason != "fabric_service_channel_binding_not_implemented" ||
payload.ServiceClass != "platform_admin" ||
payload.ServiceClass != "admin-ingress" ||
payload.ObservedAt != "2026-05-17T00:00:00Z" {
t.Fatalf("payload = %+v", payload)
}
@@ -91,13 +76,13 @@ func TestHTTPSHandlerForwardsAllowedRequestToBinder(t *testing.T) {
Config: RuntimeConfig{
ServiceType: "admin-ingress",
Scope: "platform",
ServiceClasses: []string{"platform_admin", "cluster_admin"},
ServiceClasses: []string{"admin-ingress", "admin-ingress"},
},
Binder: binder,
Now: fixedNow,
}
req := httptest.NewRequest(http.MethodPost, "https://admin.example.test/platform-admin/root?tab=nodes", strings.NewReader(`{"hello":"world"}`))
req.Header.Set("X-RAP-Service-Class", "platform_admin")
req := httptest.NewRequest(http.MethodPost, "https://admin.example.test/admin/root?tab=nodes", strings.NewReader(`{"hello":"world"}`))
req.Header.Set("X-RAP-Service-Class", "admin-ingress")
req.Header.Set("Authorization", "Bearer secret")
req.Header.Set("X-Trace-ID", "trace-1")
rec := httptest.NewRecorder()
@@ -110,9 +95,9 @@ func TestHTTPSHandlerForwardsAllowedRequestToBinder(t *testing.T) {
if rec.Header().Get("X-RAP-Result") != "accepted" || rec.Body.String() != `{"ok":true}` {
t.Fatalf("unexpected response headers=%v body=%s", rec.Header(), rec.Body.String())
}
if binder.request.ServiceClass != "platform_admin" ||
if binder.request.ServiceClass != "admin-ingress" ||
binder.request.Scope != "platform" ||
binder.request.Path != "/platform-admin/root" ||
binder.request.Path != "/admin/root" ||
binder.request.Query != "tab=nodes" ||
string(binder.request.Body) != `{"hello":"world"}` {
t.Fatalf("request = %+v", binder.request)
@@ -128,12 +113,12 @@ func TestHTTPSHandlerDerivesFabricScopeFromServiceClass(t *testing.T) {
Config: RuntimeConfig{
ServiceType: "admin-ingress",
Scope: "platform",
ServiceClasses: []string{"platform_admin", "cluster_admin"},
ServiceClasses: []string{"admin-ingress", "admin-ingress"},
},
Binder: binder,
Now: fixedNow,
}
req := httptest.NewRequest(http.MethodGet, "https://admin.example.test/cluster-admin/ui-manifest", nil)
req := httptest.NewRequest(http.MethodGet, "https://admin.example.test/clusters/ui-manifest", nil)
rec := httptest.NewRecorder()
runtime.HTTPSHandler().ServeHTTP(rec, req)
@@ -141,18 +126,18 @@ func TestHTTPSHandlerDerivesFabricScopeFromServiceClass(t *testing.T) {
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
if binder.request.ServiceClass != "cluster_admin" || binder.request.Scope != "cluster" {
if binder.request.ServiceClass != "admin-ingress" || binder.request.Scope != "cluster" {
t.Fatalf("request = %+v", binder.request)
}
}
func TestHTTPSHandlerReportsBinderFailure(t *testing.T) {
runtime := Runtime{
Config: RuntimeConfig{ServiceType: "admin-ingress", Scope: "platform", ServiceClasses: []string{"platform_admin"}},
Config: RuntimeConfig{ServiceType: "admin-ingress", Scope: "platform", ServiceClasses: []string{"admin-ingress"}},
Binder: failingBinder{},
Now: fixedNow,
}
req := httptest.NewRequest(http.MethodPost, "https://admin.example.test/platform-admin/root", nil)
req := httptest.NewRequest(http.MethodPost, "https://admin.example.test/admin/root", nil)
rec := httptest.NewRecorder()
runtime.HTTPSHandler().ServeHTTP(rec, req)