package webingress import ( "context" "encoding/json" "net/http" "strings" "time" ) const AdminRuntimeResponseSchema = "rap.web_ingress.admin_runtime_response.v1" const ControlAPIProjectionRequestSchema = "rap.web_ingress.control_api_projection_request.v1" const ControlAPIProjectionResponseSchema = "rap.web_ingress.control_api_projection_response.v1" type AdminRuntimeDispatcher struct { ProjectionClient ControlAPIProjectionClient Now func() time.Time } type ControlAPIProjectionClient interface { Project(ctx context.Context, request ControlAPIProjectionRequest) (ControlAPIProjectionResponse, error) } type ControlAPIProjectionRequest struct { SchemaVersion string `json:"schema_version"` Method string `json:"method"` Path string `json:"path"` Query string `json:"query,omitempty"` Host string `json:"host,omitempty"` Scope string `json:"scope"` ServiceClass string `json:"service_class"` ObservedAt string `json:"observed_at"` } type ControlAPIProjectionResponse struct { SchemaVersion string `json:"schema_version"` Status string `json:"status"` Reason string `json:"reason,omitempty"` StatusCode int `json:"status_code"` Headers map[string]string `json:"headers,omitempty"` Body json.RawMessage `json:"body,omitempty"` } type AdminRuntimeJSONResponse struct { SchemaVersion string `json:"schema_version"` Status string `json:"status"` Reason string `json:"reason,omitempty"` Scope string `json:"scope,omitempty"` ServiceClass string `json:"service_class,omitempty"` Path string `json:"path,omitempty"` Manifest map[string]any `json:"manifest,omitempty"` ObservedAt string `json:"observed_at"` } func (d AdminRuntimeDispatcher) HandleFabricRequest(ctx context.Context, request FabricRequest) (FabricResponse, error) { method := strings.ToUpper(strings.TrimSpace(request.Method)) path := normalizeRuntimePath(request.Path) if method == "" { method = http.MethodGet } if !allowedAdminRuntimeScope(strings.TrimSpace(request.Scope), strings.TrimSpace(request.ServiceClass)) { return d.json(http.StatusForbidden, request, "blocked", "admin_runtime_scope_rejected", nil), nil } switch { case method == http.MethodGet && (path == "/healthz" || path == "/readyz"): return d.json(http.StatusOK, request, "ready", "admin_runtime_ready", nil), nil case d.ProjectionClient != nil && (method == http.MethodGet || method == http.MethodHead): return d.project(ctx, request) case method == http.MethodGet && (path == "/ui-manifest" || strings.HasSuffix(path, "/ui-manifest")): return d.json(http.StatusOK, request, "ready", "ui_manifest_ready", d.manifest(request)), nil case method != http.MethodGet && method != http.MethodHead: return d.json(http.StatusForbidden, request, "blocked", "control_api_mutation_binding_not_implemented", nil), nil default: return d.json(http.StatusNotImplemented, request, "blocked", "control_api_projection_binding_not_implemented", nil), nil } } func allowedAdminRuntimeScope(scope string, serviceClass string) bool { switch serviceClass { case "admin-ingress": return scope == "platform" || scope == "cluster" case "public-ingress": return scope == "organization" || scope == "user" default: return false } } func (d AdminRuntimeDispatcher) project(ctx context.Context, request FabricRequest) (FabricResponse, error) { response, err := d.ProjectionClient.Project(ctx, ControlAPIProjectionRequest{ SchemaVersion: ControlAPIProjectionRequestSchema, Method: strings.ToUpper(strings.TrimSpace(request.Method)), Path: normalizeRuntimePath(request.Path), Query: request.Query, Host: request.Host, Scope: request.Scope, ServiceClass: request.ServiceClass, ObservedAt: d.observedAt(), }) if err != nil { return d.json(http.StatusBadGateway, request, "blocked", "control_api_projection_failed", nil), nil } if response.SchemaVersion != ControlAPIProjectionResponseSchema { return d.json(http.StatusBadGateway, request, "blocked", "control_api_projection_invalid_response", nil), nil } headers := http.Header{"Content-Type": []string{"application/json"}} for key, value := range response.Headers { if safeResponseHeader(key) && strings.TrimSpace(value) != "" { headers.Set(key, value) } } statusCode := response.StatusCode if statusCode < 100 || statusCode > 599 { statusCode = http.StatusOK } return FabricResponse{StatusCode: statusCode, Headers: headers, Body: append([]byte(nil), response.Body...)}, nil } func (d AdminRuntimeDispatcher) json(statusCode int, request FabricRequest, status string, reason string, manifest map[string]any) FabricResponse { payload, _ := json.Marshal(AdminRuntimeJSONResponse{ SchemaVersion: AdminRuntimeResponseSchema, Status: status, Reason: reason, Scope: request.Scope, ServiceClass: request.ServiceClass, Path: request.Path, Manifest: manifest, ObservedAt: d.observedAt(), }) return FabricResponse{ StatusCode: statusCode, Headers: http.Header{"Content-Type": []string{"application/json"}}, Body: payload, } } func (d AdminRuntimeDispatcher) manifest(request FabricRequest) map[string]any { serviceClass := strings.TrimSpace(request.ServiceClass) sections := []string{} actions := []string{} switch serviceClass { case "admin-ingress": sections = []string{"clusters", "nodes", "roles", "fabric", "workloads", "audit"} 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"} 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"} } return map[string]any{ "schema_version": "rap.web_ingress.ui_manifest.v1", "scope": request.Scope, "service_class": serviceClass, "sections": sections, "allowed_actions": actions, "mutation_enabled": false, "projection_binding": "control_api_not_bound", } } func (d AdminRuntimeDispatcher) observedAt() string { now := time.Now().UTC() if d.Now != nil { now = d.Now().UTC() } return now.Format(time.RFC3339Nano) } func normalizeRuntimePath(path string) string { path = strings.TrimSpace(path) if path == "" { return "/" } if !strings.HasPrefix(path, "/") { path = "/" + path } return path }