Files
m 20d361a886
build / backend (push) Has been cancelled
build / node-agent (push) Has been cancelled
build / worker (push) Has been cancelled
рабочий вариант, но скороть 10 МБит
2026-05-22 21:46:49 +03:00

191 lines
6.8 KiB
Go

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
}