191 lines
6.8 KiB
Go
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
|
|
}
|